aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/header
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-02-04 16:44:53 +0100
committerRigel Kent <sendmemail@rigelk.eu>2020-02-13 16:35:24 +0100
commit9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110 (patch)
treef38e6f83a9d892a99f930c0a25b1a405e679cd4a /client/src/app/header
parentece3029bd99a76b3c48a1cc8c58914c5cf61f106 (diff)
downloadPeerTube-9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110.tar.gz
PeerTube-9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110.tar.zst
PeerTube-9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110.zip
Improve search typeahead performance and use native events
Diffstat (limited to 'client/src/app/header')
-rw-r--r--client/src/app/header/header.component.html8
-rw-r--r--client/src/app/header/header.component.scss24
-rw-r--r--client/src/app/header/header.component.ts16
-rw-r--r--client/src/app/header/search-typeahead.component.html21
-rw-r--r--client/src/app/header/search-typeahead.component.scss30
-rw-r--r--client/src/app/header/search-typeahead.component.ts86
-rw-r--r--client/src/app/header/suggestion.component.html12
-rw-r--r--client/src/app/header/suggestion.component.ts18
-rw-r--r--client/src/app/header/suggestions.component.html6
-rw-r--r--client/src/app/header/suggestions.component.ts14
10 files changed, 90 insertions, 145 deletions
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html
index 8e1d24ea8..49e219187 100644
--- a/client/src/app/header/header.component.html
+++ b/client/src/app/header/header.component.html
@@ -1,10 +1,4 @@
1<my-search-typeahead class="w-100 d-flex justify-content-end"> 1<my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead>
2 <input
3 type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch"
4 i18n-placeholder placeholder="Search videos, channels…" [(ngModel)]="searchValue"
5 >
6 <span class="icon icon-search"></span>
7</my-search-typeahead>
8 2
9<a class="upload-button" routerLink="/videos/upload"> 3<a class="upload-button" routerLink="/videos/upload">
10 <my-global-icon iconName="upload"></my-global-icon> 4 <my-global-icon iconName="upload"></my-global-icon>
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss
index 2f0a407fc..91b390773 100644
--- a/client/src/app/header/header.component.scss
+++ b/client/src/app/header/header.component.scss
@@ -5,30 +5,6 @@ my-search-typeahead {
5 margin-right: 15px; 5 margin-right: 15px;
6} 6}
7 7
8#search-video {
9 @include peertube-input-text($search-input-width);
10 padding-left: 10px;
11 padding-right: 40px; // For the search icon
12 font-size: 14px;
13
14 &::placeholder {
15 color: var(--inputPlaceholderColor);
16 }
17}
18
19.icon.icon-search {
20 @include icon(25px);
21 height: 21px;
22
23 background-color: var(--mainForegroundColor);
24 mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
25
26 // yolo
27 position: absolute;
28 margin-left: -35px;
29 margin-top: 5px;
30}
31
32.upload-button { 8.upload-button {
33 @include peertube-button-link; 9 @include peertube-button-link;
34 @include orange-button; 10 @include orange-button;
diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts
index d9311c554..cce76b0d1 100644
--- a/client/src/app/header/header.component.ts
+++ b/client/src/app/header/header.component.ts
@@ -1,5 +1,4 @@
1import { Component, OnInit } from '@angular/core' 1import { Component } from '@angular/core'
2import { I18n } from '@ngx-translate/i18n-polyfill'
3 2
4@Component({ 3@Component({
5 selector: 'my-header', 4 selector: 'my-header',
@@ -7,15 +6,4 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
7 styleUrls: [ './header.component.scss' ] 6 styleUrls: [ './header.component.scss' ]
8}) 7})
9 8
10export class HeaderComponent implements OnInit { 9export class HeaderComponent {}
11 searchValue = ''
12 ariaLabelTextForSearch = ''
13
14 constructor (
15 private i18n: I18n
16 ) {}
17
18 ngOnInit () {
19 this.ariaLabelTextForSearch = this.i18n('Search videos, channels')
20 }
21}
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
index 2623ba337..428246585 100644
--- a/client/src/app/header/search-typeahead.component.html
+++ b/client/src/app/header/search-typeahead.component.html
@@ -1,9 +1,13 @@
1<div class="d-inline-flex position-relative" id="typeahead-container" #contentWrapper> 1<div class="d-inline-flex position-relative" id="typeahead-container">
2 <ng-content></ng-content> 2 <input
3 type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…"
4 [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keyup)="handleKeyUp($event)"
5 >
6 <span class="icon icon-search" (click)="doSearch()"></span>
3 7
4 <div class="position-absolute jump-to-suggestions"> 8 <div class="position-absolute jump-to-suggestions">
5 <!-- suggestions --> 9 <!-- suggestions -->
6 <my-suggestions *ngIf="hasSearch && newSearch" [results]="results" [highlight]="searchInput.value" (init)="initKeyboardEventsManager($event)" #suggestions></my-suggestions> 10 <my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions>
7 11
8 <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion --> 12 <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
9 <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden"> 13 <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden">
@@ -11,7 +15,7 @@
11 <div class="d-flex justify-content-between"> 15 <div class="d-flex justify-content-between">
12 <label class="small-title" i18n>Global search</label> 16 <label class="small-title" i18n>Global search</label>
13 <div class="advanced-search-status text-muted"> 17 <div class="advanced-search-status text-muted">
14 <span class="mr-1" i18n>using {{ globalSearchIndex }}</span> 18 <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span>
15 <i class="glyphicon glyphicon-globe"></i> 19 <i class="glyphicon glyphicon-globe"></i>
16 </div> 20 </div>
17 </div> 21 </div>
@@ -20,13 +24,14 @@
20 </div> 24 </div>
21 25
22 <!-- search instructions, when search input is empty --> 26 <!-- search instructions, when search input is empty -->
23 <div *ngIf="!hasSearch" id="typeahead-instructions" class="overflow-hidden"> 27 <div *ngIf="showInstructions" id="typeahead-instructions" class="overflow-hidden">
24 <div class="d-flex justify-content-between"> 28 <div class="d-flex justify-content-between">
25 <label class="small-title" i18n>Advanced search</label> 29 <label class="small-title" i18n>Advanced search</label>
26 <div class="advanced-search-status c-help"> 30 <div class="advanced-search-status c-help">
27 <span [ngClass]="URIPolicy === 'any' ? 'text-success' : 'text-muted'" [title]="URIPolicyText"> 31 <span [ngClass]="anyURI ? '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.">
28 <span class="mr-1" i18n>{URIPolicy, select, only-followed {only followed instances} other {any instance}} </span> 32 <span *ngIf="anyURI" class="mr-1" i18n>any instance</span>
29 <i [ngClass]="URIPolicy === 'any' ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i> 33 <span *ngIf="!anyURI" class="mr-1" i18n>only followed instances</span>
34 <i [ngClass]="anyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
30 </span> 35 </span>
31 </div> 36 </div>
32 </div> 37 </div>
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss
index 6d7511c70..a55e78326 100644
--- a/client/src/app/header/search-typeahead.component.scss
+++ b/client/src/app/header/search-typeahead.component.scss
@@ -3,6 +3,30 @@
3@import '_bootstrap-variables'; 3@import '_bootstrap-variables';
4@import '~bootstrap/scss/mixins/_breakpoints'; 4@import '~bootstrap/scss/mixins/_breakpoints';
5 5
6#search-video {
7 @include peertube-input-text($search-input-width);
8 padding-left: 10px;
9 padding-right: 40px; // For the search icon
10 font-size: 14px;
11
12 &::placeholder {
13 color: var(--inputPlaceholderColor);
14 }
15}
16
17.icon.icon-search {
18 @include icon(25px);
19 height: 21px;
20
21 background-color: var(--mainForegroundColor);
22 mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
23
24 // yolo
25 position: absolute;
26 margin-left: -35px;
27 margin-top: 5px;
28}
29
6.jump-to-suggestions { 30.jump-to-suggestions {
7 top: 100%; 31 top: 100%;
8 left: 0; 32 left: 0;
@@ -42,7 +66,7 @@ my-suggestions ::ng-deep ul {
42} 66}
43 67
44#typeahead-container { 68#typeahead-container {
45 ::ng-deep input { 69 input {
46 border: 1px solid var(--mainBackgroundColor) !important; 70 border: 1px solid var(--mainBackgroundColor) !important;
47 box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px; 71 box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
48 flex-grow: 1; 72 flex-grow: 1;
@@ -56,12 +80,12 @@ my-suggestions ::ng-deep ul {
56 @media screen and (max-width: $small-view) { 80 @media screen and (max-width: $small-view) {
57 flex: 1; 81 flex: 1;
58 82
59 ::ng-deep input { 83 input {
60 width: unset; 84 width: unset;
61 } 85 }
62 } 86 }
63 87
64 ::ng-deep span { 88 span {
65 right: 10px; 89 right: 10px;
66 } 90 }
67 91
diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts
index 9b414bc56..c265f2c83 100644
--- a/client/src/app/header/search-typeahead.component.ts
+++ b/client/src/app/header/search-typeahead.component.ts
@@ -1,16 +1,15 @@
1import { 1import {
2 Component, 2 Component,
3 ViewChild,
4 ElementRef,
5 AfterViewInit, 3 AfterViewInit,
6 OnInit, 4 OnInit,
7 OnDestroy, 5 OnDestroy,
8 QueryList 6 QueryList,
7 ViewChild,
8 ElementRef
9} from '@angular/core' 9} from '@angular/core'
10import { Router, NavigationEnd, Params, ActivatedRoute } from '@angular/router' 10import { Router, Params, ActivatedRoute } from '@angular/router'
11import { AuthService, ServerService } from '@app/core' 11import { AuthService, ServerService } from '@app/core'
12import { I18n } from '@ngx-translate/i18n-polyfill' 12import { first, tap } from 'rxjs/operators'
13import { filter, first, tap, map } from 'rxjs/operators'
14import { ListKeyManager } from '@angular/cdk/a11y' 13import { ListKeyManager } from '@angular/cdk/a11y'
15import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes' 14import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'
16import { SuggestionComponent, Result } from './suggestion.component' 15import { SuggestionComponent, Result } from './suggestion.component'
@@ -24,19 +23,16 @@ import { ServerConfig } from '@shared/models'
24 styleUrls: [ './search-typeahead.component.scss' ] 23 styleUrls: [ './search-typeahead.component.scss' ]
25}) 24})
26export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit { 25export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit {
27 @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef 26 @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement>
28 27
29 hasChannel = false 28 hasChannel = false
30 inChannel = false 29 inChannel = false
31 newSearch = true 30 newSearch = true
32 31
33 searchInput: HTMLInputElement 32 search = ''
34 serverConfig: ServerConfig 33 serverConfig: ServerConfig
35 34
36 URIPolicyText: string
37 inAllText: string
38 inThisChannelText: string 35 inThisChannelText: string
39 globalSearchIndex = 'https://index.joinpeertube.org'
40 36
41 keyboardEventsManager: ListKeyManager<SuggestionComponent> 37 keyboardEventsManager: ListKeyManager<SuggestionComponent>
42 results: any[] = [] 38 results: any[] = []
@@ -45,30 +41,10 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
45 private authService: AuthService, 41 private authService: AuthService,
46 private router: Router, 42 private router: Router,
47 private route: ActivatedRoute, 43 private route: ActivatedRoute,
48 private serverService: ServerService, 44 private serverService: ServerService
49 private i18n: I18n 45 ) {}
50 ) {
51 this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, or if your instance only allows doing so for instances it follows.')
52 this.inAllText = this.i18n('In all PeerTube')
53 this.inThisChannelText = this.i18n('In this channel')
54 }
55 46
56 ngOnInit () { 47 ngOnInit () {
57 this.router.events
58 .pipe(filter(e => e instanceof NavigationEnd))
59 .subscribe((event: NavigationEnd) => {
60 this.hasChannel = event.url.startsWith('/videos/watch')
61 this.inChannel = event.url.startsWith('/video-channels')
62 this.computeResults()
63 })
64
65 this.router.events
66 .pipe(
67 filter(e => e instanceof NavigationEnd),
68 map(() => getParameterByName('search', window.location.href))
69 )
70 .subscribe(searchQuery => this.searchInput.value = searchQuery || '')
71
72 this.serverService.getConfig() 48 this.serverService.getConfig()
73 .subscribe(config => this.serverConfig = config) 49 .subscribe(config => this.serverConfig = config)
74 } 50 }
@@ -78,53 +54,52 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
78 } 54 }
79 55
80 ngAfterViewInit () { 56 ngAfterViewInit () {
81 this.searchInput = this.contentWrapper.nativeElement.childNodes[0] 57 this.search = getParameterByName('search', window.location.href) || ''
82 this.searchInput.addEventListener('input', this.computeResults.bind(this))
83 this.searchInput.addEventListener('keyup', this.handleKeyUp.bind(this))
84 }
85
86 get hasSearch () {
87 return !!this.searchInput && !!this.searchInput.value
88 } 58 }
89 59
90 get activeResult () { 60 get activeResult () {
91 return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result 61 return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result
92 } 62 }
93 63
64 get showInstructions () {
65 return !this.search
66 }
67
94 get showHelp () { 68 get showHelp () {
95 return this.hasSearch && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false 69 return this.search && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false
96 } 70 }
97 71
98 get URIPolicy (): 'only-followed' | 'any' { 72 get anyURI () {
99 return ( 73 if (!this.serverConfig) return false
100 this.authService.isLoggedIn() 74 return this.authService.isLoggedIn()
101 ? this.serverConfig.search.remoteUri.users 75 ? this.serverConfig.search.remoteUri.users
102 : this.serverConfig.search.remoteUri.anonymous 76 : this.serverConfig.search.remoteUri.anonymous
103 ) 77 }
104 ? 'any' 78
105 : 'only-followed' 79 onSearchChange () {
80 this.computeResults()
106 } 81 }
107 82
108 computeResults () { 83 computeResults () {
109 this.newSearch = true 84 this.newSearch = true
110 let results: Result[] = [] 85 let results: Result[] = []
111 86
112 if (this.hasSearch) { 87 if (this.search) {
113 results = [ 88 results = [
114 /* Channel search is still unimplemented. Uncomment when it is. 89 /* Channel search is still unimplemented. Uncomment when it is.
115 { 90 {
116 text: this.searchInput.value, 91 text: this.search,
117 type: 'search-channel' 92 type: 'search-channel'
118 }, 93 },
119 */ 94 */
120 { 95 {
121 text: this.searchInput.value, 96 text: this.search,
122 type: 'search-instance', 97 type: 'search-instance',
123 default: true 98 default: true
124 }, 99 },
125 /* Global search is still unimplemented. Uncomment when it is. 100 /* Global search is still unimplemented. Uncomment when it is.
126 { 101 {
127 text: this.searchInput.value, 102 text: this.search,
128 type: 'search-global' 103 type: 'search-global'
129 }, 104 },
130 */ 105 */
@@ -137,7 +112,8 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
137 // if we're not in a channel or one of its videos/playlits, show all channel-related results 112 // if we're not in a channel or one of its videos/playlits, show all channel-related results
138 if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') 113 if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel')
139 // if we're in a channel, show all channel-related results except for the channel redirection itself 114 // if we're in a channel, show all channel-related results except for the channel redirection itself
140 if (this.inChannel) return !(result.type === 'channel') 115 if (this.inChannel) return result.type !== 'channel'
116 // all other result types are kept
141 return true 117 return true
142 } 118 }
143 ) 119 )
@@ -187,7 +163,7 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
187 Object.assign(queryParams, this.route.snapshot.queryParams) 163 Object.assign(queryParams, this.route.snapshot.queryParams)
188 } 164 }
189 165
190 Object.assign(queryParams, { search: this.searchInput.value }) 166 Object.assign(queryParams, { search: this.search })
191 167
192 const o = this.authService.isLoggedIn() 168 const o = this.authService.isLoggedIn()
193 ? this.loadUserLanguagesIfNeeded(queryParams) 169 ? this.loadUserLanguagesIfNeeded(queryParams)
diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html
index 894cacb95..edde2023a 100644
--- a/client/src/app/header/suggestion.component.html
+++ b/client/src/app/header/suggestion.component.html
@@ -9,15 +9,9 @@
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> 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>
10 10
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"> 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">
12 <span *ngIf="result.type === 'search-channel'" [attr.aria-label]="inThisChannelText"> 12 <span *ngIf="result.type === 'search-channel'" i18n>In this channel</span>
13 {{ inThisChannelText }} 13 <span *ngIf="result.type === 'search-instance'" i18n>In this instance</span>
14 </span> 14 <span *ngIf="result.type === 'search-global'" i18n>In the vidiverse</span>
15 <span *ngIf="result.type === 'search-instance'" [attr.aria-label]="inThisInstanceText">
16 {{ inThisInstanceText }}
17 </span>
18 <span *ngIf="result.type === 'search-global'" [attr.aria-label]="inAllText">
19 {{ inAllText }}
20 </span>
21 <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span> 15 <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
22 </div> 16 </div>
23 17
diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts
index bdcb3e03f..69641b511 100644
--- a/client/src/app/header/suggestion.component.ts
+++ b/client/src/app/header/suggestion.component.ts
@@ -1,6 +1,5 @@
1import { Input, Component, Output, EventEmitter, OnInit } from '@angular/core' 1import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core'
2import { RouterLink } from '@angular/router' 2import { RouterLink } from '@angular/router'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { ListKeyManagerOption } from '@angular/cdk/a11y' 3import { ListKeyManagerOption } from '@angular/cdk/a11y'
5 4
6export type Result = { 5export type Result = {
@@ -13,28 +12,17 @@ export type Result = {
13@Component({ 12@Component({
14 selector: 'my-suggestion', 13 selector: 'my-suggestion',
15 templateUrl: './suggestion.component.html', 14 templateUrl: './suggestion.component.html',
16 styleUrls: [ './suggestion.component.scss' ] 15 styleUrls: [ './suggestion.component.scss' ],
16 changeDetection: ChangeDetectionStrategy.OnPush
17}) 17})
18export class SuggestionComponent implements OnInit, ListKeyManagerOption { 18export class SuggestionComponent implements OnInit, ListKeyManagerOption {
19 @Input() result: Result 19 @Input() result: Result
20 @Input() highlight: string 20 @Input() highlight: string
21 @Output() selected = new EventEmitter() 21 @Output() selected = new EventEmitter()
22 22
23 inAllText: string
24 inThisChannelText: string
25 inThisInstanceText: string
26
27 disabled = false 23 disabled = false
28 active = false 24 active = false
29 25
30 constructor (
31 private i18n: I18n
32 ) {
33 this.inAllText = this.i18n('In the vidiverse')
34 this.inThisChannelText = this.i18n('In this channel')
35 this.inThisInstanceText = this.i18n('In this instance')
36 }
37
38 getLabel () { 26 getLabel () {
39 return this.result.text 27 return this.result.text
40 } 28 }
diff --git a/client/src/app/header/suggestions.component.html b/client/src/app/header/suggestions.component.html
new file mode 100644
index 000000000..8d017d78d
--- /dev/null
+++ b/client/src/app/header/suggestions.component.html
@@ -0,0 +1,6 @@
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
index fac7fe2f9..ee3ef73c2 100644
--- a/client/src/app/header/suggestions.component.ts
+++ b/client/src/app/header/suggestions.component.ts
@@ -1,16 +1,10 @@
1import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren } from '@angular/core' 1import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core'
2import { SuggestionComponent } from './suggestion.component' 2import { SuggestionComponent } from './suggestion.component'
3 3
4@Component({ 4@Component({
5 selector: 'my-suggestions', 5 selector: 'my-suggestions',
6 template: ` 6 templateUrl: './suggestions.component.html',
7 <ul role="listbox" class="p-0 m-0"> 7 changeDetection: ChangeDetectionStrategy.OnPush
8 <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
9 role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
10 <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
11 </li>
12 </ul>
13 `
14}) 8})
15export class SuggestionsComponent implements AfterViewInit { 9export class SuggestionsComponent implements AfterViewInit {
16 @Input() results: any[] 10 @Input() results: any[]
@@ -20,7 +14,7 @@ export class SuggestionsComponent implements AfterViewInit {
20 14
21 ngAfterViewInit () { 15 ngAfterViewInit () {
22 this.listItems.changes.subscribe( 16 this.listItems.changes.subscribe(
23 val => this.init.emit({ items: this.listItems }) 17 _ => this.init.emit({ items: this.listItems })
24 ) 18 )
25 } 19 }
26 20