aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/header
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/header')
-rw-r--r--client/src/app/header/header.component.html6
-rw-r--r--client/src/app/header/header.component.scss49
-rw-r--r--client/src/app/header/header.component.ts60
-rw-r--r--client/src/app/header/index.ts3
-rw-r--r--client/src/app/header/search-typeahead.component.html53
-rw-r--r--client/src/app/header/search-typeahead.component.scss145
-rw-r--r--client/src/app/header/search-typeahead.component.ts179
-rw-r--r--client/src/app/header/suggestion.component.html22
-rw-r--r--client/src/app/header/suggestion.component.scss32
-rw-r--r--client/src/app/header/suggestion.component.ts37
-rw-r--r--client/src/app/header/suggestions.component.html6
-rw-r--r--client/src/app/header/suggestions.component.ts24
12 files changed, 505 insertions, 111 deletions
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html
index 4fd18f9bd..49e219187 100644
--- a/client/src/app/header/header.component.html
+++ b/client/src/app/header/header.component.html
@@ -1,8 +1,4 @@
1<input 1<my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead>
2 type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search videos, channels…"
3 [(ngModel)]="searchValue" (keyup.enter)="doSearch()"
4>
5<span (click)="doSearch()" class="icon icon-search"></span>
6 2
7<a class="upload-button" routerLink="/videos/upload"> 3<a class="upload-button" routerLink="/videos/upload">
8 <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 2bbde74bc..91b390773 100644
--- a/client/src/app/header/header.component.scss
+++ b/client/src/app/header/header.component.scss
@@ -1,51 +1,8 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4#search-video { 4my-search-typeahead {
5 @include peertube-input-text($search-input-width);
6 padding-left: 10px;
7 margin-right: 15px; 5 margin-right: 15px;
8 padding-right: 40px; // For the search icon
9 font-size: 14px;
10
11 transition: box-shadow .3s ease;
12
13 /* light border style */
14 border: 1px solid var(--mainBackgroundColor);
15 box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
16
17 &:focus {
18 box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px;
19 }
20
21 &::placeholder {
22 color: var(--inputPlaceholderColor);
23 }
24
25 &:focus::placeholder {
26 opacity: 0 !important;
27 }
28
29 @media screen and (max-width: 800px) {
30 width: calc(100% - 150px);
31 }
32
33 @media screen and (max-width: 600px) {
34 width: calc(100% - 70px);
35 }
36}
37
38.icon.icon-search {
39 @include icon(25px);
40 height: 21px;
41
42 background-color: var(--mainForegroundColor);
43 mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
44
45 // yolo
46 position: absolute;
47 margin-left: -50px;
48 margin-top: 5px;
49} 6}
50 7
51.upload-button { 8.upload-button {
@@ -56,10 +13,6 @@
56 color: var(--mainBackgroundColor) !important; 13 color: var(--mainBackgroundColor) !important;
57 margin-right: 25px; 14 margin-right: 25px;
58 15
59 @media screen and (max-width: 800px) {
60 margin-right: 0;
61 }
62
63 @media screen and (max-width: 600px) { 16 @media screen and (max-width: 600px) {
64 margin-right: 10px; 17 margin-right: 10px;
65 padding: 0 10px; 18 padding: 0 10px;
diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts
index 92a7eded6..cce76b0d1 100644
--- a/client/src/app/header/header.component.ts
+++ b/client/src/app/header/header.component.ts
@@ -1,10 +1,4 @@
1import { filter, first, map, tap } from 'rxjs/operators' 1import { Component } from '@angular/core'
2import { Component, OnInit } from '@angular/core'
3import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router'
4import { getParameterByName } from '../shared/misc/utils'
5import { AuthService, Notifier, ServerService } from '@app/core'
6import { of } from 'rxjs'
7import { I18n } from '@ngx-translate/i18n-polyfill'
8 2
9@Component({ 3@Component({
10 selector: 'my-header', 4 selector: 'my-header',
@@ -12,54 +6,4 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
12 styleUrls: [ './header.component.scss' ] 6 styleUrls: [ './header.component.scss' ]
13}) 7})
14 8
15export class HeaderComponent implements OnInit { 9export class HeaderComponent {}
16 searchValue = ''
17 ariaLabelTextForSearch = ''
18
19 constructor (
20 private router: Router,
21 private route: ActivatedRoute,
22 private auth: AuthService,
23 private serverService: ServerService,
24 private authService: AuthService,
25 private notifier: Notifier,
26 private i18n: I18n
27 ) {}
28
29 ngOnInit () {
30 this.ariaLabelTextForSearch = this.i18n('Search videos, channels')
31
32 this.router.events
33 .pipe(
34 filter(e => e instanceof NavigationEnd),
35 map(() => getParameterByName('search', window.location.href))
36 )
37 .subscribe(searchQuery => this.searchValue = searchQuery || '')
38 }
39
40 doSearch () {
41 const queryParams: Params = {}
42
43 if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
44 Object.assign(queryParams, this.route.snapshot.queryParams)
45 }
46
47 Object.assign(queryParams, { search: this.searchValue })
48
49 const o = this.auth.isLoggedIn()
50 ? this.loadUserLanguagesIfNeeded(queryParams)
51 : of(true)
52
53 o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
54 }
55
56 private loadUserLanguagesIfNeeded (queryParams: any) {
57 if (queryParams && queryParams.languageOneOf) return of(queryParams)
58
59 return this.auth.userInformationLoaded
60 .pipe(
61 first(),
62 tap(() => Object.assign(queryParams, { languageOneOf: this.auth.getUser().videoLanguages }))
63 )
64 }
65}
diff --git a/client/src/app/header/index.ts b/client/src/app/header/index.ts
index d98d2d00a..a882d4d1f 100644
--- a/client/src/app/header/index.ts
+++ b/client/src/app/header/index.ts
@@ -1 +1,4 @@
1export * from './header.component' 1export * from './header.component'
2export * from './search-typeahead.component'
3export * from './suggestions.component'
4export * from './suggestion.component'
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
new file mode 100644
index 000000000..710268664
--- /dev/null
+++ b/client/src/app/header/search-typeahead.component.html
@@ -0,0 +1,53 @@
1<div class="d-inline-flex position-relative" id="typeahead-container">
2 <input
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()"
5 >
6 <span class="icon icon-search" (click)="doSearch()"></span>
7
8 <div class="position-absolute jump-to-suggestions">
9 <!-- suggestions -->
10 <my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions>
11
12 <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
13 <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden">
14 <ng-container *ngIf="activeResult.type === 'search-global'">
15 <div class="d-flex justify-content-between">
16 <label class="small-title" i18n>GLOBAL SEARCH</label>
17 <div class="advanced-search-status text-muted">
18 <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span>
19 <i class="glyphicon glyphicon-globe"></i>
20 </div>
21 </div>
22 <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>
23 </ng-container>
24 </div>
25
26 <!-- search instructions, when search input is empty -->
27 <div *ngIf="areInstructionsDisplayed" id="typeahead-instructions" class="overflow-hidden">
28 <div class="d-flex justify-content-between">
29 <label class="small-title" i18n>ADVANCED SEARCH</label>
30 <div class="advanced-search-status c-help">
31 <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.">
32 <span *ngIf="canSearchAnyURI" class="mr-1" i18n>any instance</span>
33 <span *ngIf="!canSearchAnyURI" class="mr-1" i18n>only followed instances</span>
34 <i [ngClass]="canSearchAnyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
35 </span>
36 </div>
37 </div>
38 <ul>
39 <li>
40 <em>@channel_id@domain</em> <span class="flex-auto text-muted" i18n>channel</span>
41 </li>
42 <li>
43 <em>URL</em> <span class="text-muted" i18n>channel</span>
44 </li>
45 <li>
46 <em>UUID</em> <span class="text-muted" i18n>video</span>
47 </li>
48 </ul>
49 <span class="text-muted" i18n>Any other text will return matching video or channel names.</span>
50 </div>
51 </div>
52
53</div>
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss
new file mode 100644
index 000000000..33b88825f
--- /dev/null
+++ b/client/src/app/header/search-typeahead.component.scss
@@ -0,0 +1,145 @@
1@import '_mixins';
2@import '_variables';
3@import '_bootstrap-variables';
4@import '~bootstrap/scss/mixins/_breakpoints';
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
30.jump-to-suggestions {
31 top: 100%;
32 left: 0;
33 z-index: z(typeahead);
34 width: 100%;
35}
36
37#typeahead-help,
38#typeahead-instructions,
39my-suggestions ::ng-deep ul {
40 border: 1px solid var(--mainBackgroundColor);
41 border-bottom-right-radius: 3px;
42 border-bottom-left-radius: 3px;
43 background: var(--mainBackgroundColor);
44 transition: .3s ease;
45 transition-property: box-shadow;
46}
47
48#typeahead-help,
49#typeahead-instructions {
50 margin-top: 10px;
51 width: 100%;
52 padding: .5rem 1rem;
53 white-space: normal;
54
55 ul {
56 list-style: none;
57 padding: 0;
58 margin-bottom: .5rem;
59
60 em {
61 font-weight: 600;
62 margin-right: 0.2rem;
63 font-style: normal;
64 }
65 }
66}
67
68#typeahead-container {
69 input {
70 border: 1px solid var(--mainBackgroundColor) !important;
71 box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
72 flex-grow: 1;
73 transition: box-shadow .3s ease, width .2s ease;
74 }
75
76 @media screen and (min-width: $mobile-view) {
77 margin-left: 10px;
78 }
79
80 @media screen and (max-width: $small-view) {
81 flex: 1;
82
83 input {
84 width: unset;
85 }
86 }
87
88 span {
89 right: 10px;
90 }
91
92 & > div:last-child {
93 // we have to switch the display and not the opacity,
94 // to avoid clashing with the rest of the interface.
95 display: none;
96 }
97
98 &:focus,
99 ::ng-deep &:focus-within {
100 & > div:last-child {
101 @media screen and (min-width: $mobile-view) {
102 display: initial !important;
103 }
104
105 #typeahead-help,
106 #typeahead-instructions,
107 my-suggestions ::ng-deep ul {
108 box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px;
109 }
110 }
111
112 ::ng-deep input {
113 box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px;
114 border-end-start-radius: 0;
115 border-end-end-radius: 0;
116
117 @include media-breakpoint-up(lg) {
118 width: 500px;
119 }
120 }
121 }
122}
123
124.glyphicon {
125 top: 3px;
126}
127
128.advanced-search-status {
129 height: max-content;
130 cursor: default;
131
132 &.c-help {
133 cursor: help;
134 }
135}
136
137.small-title {
138 @include in-content-small-title;
139
140 margin-bottom: .5rem;
141}
142
143::ng-deep my-suggestion {
144 width: 100%;
145}
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..2bf1072f4
--- /dev/null
+++ b/client/src/app/header/search-typeahead.component.ts
@@ -0,0 +1,179 @@
1import { Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core'
2import { ActivatedRoute, Params, Router } from '@angular/router'
3import { 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'
9
10@Component({
11 selector: 'my-search-typeahead',
12 templateUrl: './search-typeahead.component.html',
13 styleUrls: [ './search-typeahead.component.scss' ]
14})
15export 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 this.route.queryParams
39 .pipe(first(params => this.isOnSearch() && params.search !== undefined && params.search !== null))
40 .subscribe(params => this.search = params.search)
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 handleKey (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 }
147 }
148
149 isOnSearch () {
150 return window.location.pathname === '/search'
151 }
152
153 doSearch () {
154 this.newSearch = false
155 const queryParams: Params = {}
156
157 if (this.isOnSearch() && this.route.snapshot.queryParams) {
158 Object.assign(queryParams, this.route.snapshot.queryParams)
159 }
160
161 Object.assign(queryParams, { search: this.search })
162
163 const o = this.authService.isLoggedIn()
164 ? this.loadUserLanguagesIfNeeded(queryParams)
165 : of(true)
166
167 o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
168 }
169
170 private loadUserLanguagesIfNeeded (queryParams: any) {
171 if (queryParams && queryParams.languageOneOf) return of(queryParams)
172
173 return this.authService.userInformationLoaded
174 .pipe(
175 first(),
176 tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
177 )
178 }
179}
diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html
new file mode 100644
index 000000000..d7ae3450a
--- /dev/null
+++ b/client/src/app/header/suggestion.component.html
@@ -0,0 +1,22 @@
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">
3 <my-global-icon *ngIf="result.type !== 'channel'" iconName="search"></my-global-icon>
4 <my-global-icon *ngIf="result.type === 'channel'" iconName="folder"></my-global-icon>
5 </div>
6
7 <img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28">
8
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
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'" i18n>In this channel</span>
13 <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-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
16 </div>
17
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.scss b/client/src/app/header/suggestion.component.scss
new file mode 100644
index 000000000..1de2f43bd
--- /dev/null
+++ b/client/src/app/header/suggestion.component.scss
@@ -0,0 +1,32 @@
1@import '_mixins';
2
3a {
4 @include disable-default-a-behaviour;
5 width: 100%;
6
7 &, &:hover {
8 color: var(--mainForegroundColor);
9
10 &.focus-visible {
11 background-color: var(--mainHoverColor);
12 color: var(--mainBackgroundColor);
13 }
14 }
15}
16
17.bg-gray {
18 background-color: var(--mainBackgroundColor);
19}
20
21.text-gray-light {
22 color: var(--mainForegroundColor);
23}
24
25my-global-icon {
26 width: 17px;
27 position: relative;
28 top: -2px;
29 margin: 5px;
30
31 @include apply-svg-color(var(--mainForegroundColor));
32}
diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts
new file mode 100644
index 000000000..69641b511
--- /dev/null
+++ b/client/src/app/header/suggestion.component.ts
@@ -0,0 +1,37 @@
1import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core'
2import { RouterLink } from '@angular/router'
3import { ListKeyManagerOption } from '@angular/cdk/a11y'
4
5export type Result = {
6 text: string
7 type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any'
8 routerLink?: RouterLink,
9 default?: boolean
10}
11
12@Component({
13 selector: 'my-suggestion',
14 templateUrl: './suggestion.component.html',
15 styleUrls: [ './suggestion.component.scss' ],
16 changeDetection: ChangeDetectionStrategy.OnPush
17})
18export class SuggestionComponent implements OnInit, ListKeyManagerOption {
19 @Input() result: Result
20 @Input() highlight: string
21 @Output() selected = new EventEmitter()
22
23 disabled = false
24 active = false
25
26 getLabel () {
27 return this.result.text
28 }
29
30 ngOnInit () {
31 if (this.result.default) this.active = true
32 }
33
34 selectItem () {
35 this.selected.emit(this.result)
36 }
37}
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
new file mode 100644
index 000000000..ee3ef73c2
--- /dev/null
+++ b/client/src/app/header/suggestions.component.ts
@@ -0,0 +1,24 @@
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}