aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-01-25 16:32:06 +0100
committerRigel Kent <sendmemail@rigelk.eu>2020-02-13 16:32:21 +0100
commit6af662a5961b48ac12682df2b8b971060a2cc67d (patch)
treede1efc71cbe8543b5b832e5de99a407a54c37220 /client
parentf409f0c3b91d85c66b4841d72ea65b5fbe1483d8 (diff)
downloadPeerTube-6af662a5961b48ac12682df2b8b971060a2cc67d.tar.gz
PeerTube-6af662a5961b48ac12682df2b8b971060a2cc67d.tar.zst
PeerTube-6af662a5961b48ac12682df2b8b971060a2cc67d.zip
Add keyboard navigation and hepler to typeahead
Diffstat (limited to 'client')
-rw-r--r--client/src/app/app.module.ts4
-rw-r--r--client/src/app/header/header.component.html2
-rw-r--r--client/src/app/header/header.component.ts5
-rw-r--r--client/src/app/header/index.ts2
-rw-r--r--client/src/app/header/search-typeahead.component.html53
-rw-r--r--client/src/app/header/search-typeahead.component.scss45
-rw-r--r--client/src/app/header/search-typeahead.component.ts70
-rw-r--r--client/src/app/header/suggestion.component.html28
-rw-r--r--client/src/app/header/suggestion.component.scss32
-rw-r--r--client/src/app/header/suggestion.component.ts48
-rw-r--r--client/src/app/header/suggestions.component.ts31
11 files changed, 234 insertions, 86 deletions
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 2db33d638..9e220a383 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -9,7 +9,7 @@ import 'focus-visible'
9import { AppRoutingModule } from './app-routing.module' 9import { AppRoutingModule } from './app-routing.module'
10import { AppComponent } from './app.component' 10import { AppComponent } from './app.component'
11import { CoreModule } from './core' 11import { CoreModule } from './core'
12import { HeaderComponent, SearchTypeaheadComponent } from './header' 12import { HeaderComponent, SearchTypeaheadComponent, SuggestionsComponent, SuggestionComponent } from './header'
13import { LoginModule } from './login' 13import { LoginModule } from './login'
14import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' 14import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
15import { SharedModule } from './shared' 15import { SharedModule } from './shared'
@@ -42,6 +42,8 @@ export function metaFactory (serverService: ServerService): MetaLoader {
42 AvatarNotificationComponent, 42 AvatarNotificationComponent,
43 HeaderComponent, 43 HeaderComponent,
44 SearchTypeaheadComponent, 44 SearchTypeaheadComponent,
45 SuggestionsComponent,
46 SuggestionComponent,
45 47
46 WelcomeModalComponent, 48 WelcomeModalComponent,
47 InstanceConfigWarningModalComponent 49 InstanceConfigWarningModalComponent
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html
index 38c87c642..074bebf21 100644
--- a/client/src/app/header/header.component.html
+++ b/client/src/app/header/header.component.html
@@ -1,6 +1,6 @@
1<my-search-typeahead> 1<my-search-typeahead>
2 <input 2 <input
3 type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search videos, channels…" 3 type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search locally videos, channels…"
4 [(ngModel)]="searchValue" (keyup.enter)="doSearch()" 4 [(ngModel)]="searchValue" (keyup.enter)="doSearch()"
5 > 5 >
6 <span (click)="doSearch()" class="icon icon-search"></span> 6 <span (click)="doSearch()" class="icon icon-search"></span>
diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts
index 92a7eded6..ca4a32cbc 100644
--- a/client/src/app/header/header.component.ts
+++ b/client/src/app/header/header.component.ts
@@ -2,7 +2,7 @@ import { filter, first, map, tap } from 'rxjs/operators'
2import { Component, OnInit } from '@angular/core' 2import { Component, OnInit } from '@angular/core'
3import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router' 3import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router'
4import { getParameterByName } from '../shared/misc/utils' 4import { getParameterByName } from '../shared/misc/utils'
5import { AuthService, Notifier, ServerService } from '@app/core' 5import { AuthService } from '@app/core'
6import { of } from 'rxjs' 6import { of } from 'rxjs'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8 8
@@ -20,9 +20,6 @@ export class HeaderComponent implements OnInit {
20 private router: Router, 20 private router: Router,
21 private route: ActivatedRoute, 21 private route: ActivatedRoute,
22 private auth: AuthService, 22 private auth: AuthService,
23 private serverService: ServerService,
24 private authService: AuthService,
25 private notifier: Notifier,
26 private i18n: I18n 23 private i18n: I18n
27 ) {} 24 ) {}
28 25
diff --git a/client/src/app/header/index.ts b/client/src/app/header/index.ts
index bf1787103..a882d4d1f 100644
--- a/client/src/app/header/index.ts
+++ b/client/src/app/header/index.ts
@@ -1,2 +1,4 @@
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'
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
index fe3f6ff4d..2623ba337 100644
--- a/client/src/app/header/search-typeahead.component.html
+++ b/client/src/app/header/search-typeahead.component.html
@@ -3,17 +3,27 @@
3 3
4 <div class="position-absolute jump-to-suggestions"> 4 <div class="position-absolute jump-to-suggestions">
5 <!-- suggestions --> 5 <!-- suggestions -->
6 <ul id="jump-to-results" role="listbox" class="p-0 m-0" #optionsList> 6 <my-suggestions *ngIf="hasSearch && newSearch" [results]="results" [highlight]="searchInput.value" (init)="initKeyboardEventsManager($event)" #suggestions></my-suggestions>
7 <li *ngFor="let res of results" class="d-flex flex-justify-start flex-items-center p-0 f5" role="option" aria-selected="true"> 7
8 <ng-container *ngTemplateOutlet="result; context: {$implicit: res}"></ng-container> 8 <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
9 </li> 9 <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden">
10 </ul> 10 <ng-container *ngIf="activeResult.type === 'search-global'">
11 <div class="d-flex justify-content-between">
12 <label class="small-title" i18n>Global search</label>
13 <div class="advanced-search-status text-muted">
14 <span class="mr-1" i18n>using {{ globalSearchIndex }}</span>
15 <i class="glyphicon glyphicon-globe"></i>
16 </div>
17 </div>
18 <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>
19 </ng-container>
20 </div>
11 21
12 <!-- search instructions, when search input is empty --> 22 <!-- search instructions, when search input is empty -->
13 <div *ngIf="!hasSearch" id="typeahead-instructions" class="overflow-hidden"> 23 <div *ngIf="!hasSearch" id="typeahead-instructions" class="overflow-hidden">
14 <div class="d-flex justify-content-between"> 24 <div class="d-flex justify-content-between">
15 <label class="small-title" i18n>Advanced search</label> 25 <label class="small-title" i18n>Advanced search</label>
16 <div class="advanced-search-status"> 26 <div class="advanced-search-status c-help">
17 <span [ngClass]="URIPolicy === 'any' ? 'text-success' : 'text-muted'" [title]="URIPolicyText"> 27 <span [ngClass]="URIPolicy === 'any' ? 'text-success' : 'text-muted'" [title]="URIPolicyText">
18 <span class="mr-1" i18n>{URIPolicy, select, only-followed {only followed instances} other {any instance}} </span> 28 <span class="mr-1" i18n>{URIPolicy, select, only-followed {only followed instances} other {any instance}} </span>
19 <i [ngClass]="URIPolicy === 'any' ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i> 29 <i [ngClass]="URIPolicy === 'any' ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
@@ -36,34 +46,3 @@
36 </div> 46 </div>
37 47
38</div> 48</div>
39
40<ng-template #result let-result>
41 <a tabindex="0" class="d-flex flex-auto flex-items-center p-2"
42 data-target-type="Repository"
43 [routerLink]="result.routerLink"
44 >
45 <div class="flex-shrink-0 mr-2 text-center">
46 <my-global-icon *ngIf="result.type !== 'channel'" iconName="search"></my-global-icon>
47 <my-global-icon *ngIf="result.type === 'channel'" iconName="folder"></my-global-icon>
48 </div>
49
50 <img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28">
51
52 <div class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : searchInput.value"></div>
53
54 <div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6">
55 <span *ngIf="result.type === 'search-channel'" [attr.aria-label]="inThisChannelText">
56 {{ inThisChannelText }}
57 </span>
58 <span *ngIf="result.type === 'search-global'" [attr.aria-label]="inAllText">
59 {{ inAllText }}
60 </span>
61 <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
62 </div>
63
64 <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>
65 Jump to channel
66 <span class="d-inline-block ml-1 v-align-middle">↵</span>
67 </div>
68 </a>
69</ng-template> \ No newline at end of file
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss
index 93f021e33..c410d4734 100644
--- a/client/src/app/header/search-typeahead.component.scss
+++ b/client/src/app/header/search-typeahead.component.scss
@@ -7,8 +7,9 @@
7 width: 100%; 7 width: 100%;
8} 8}
9 9
10#typeahead-help,
10#typeahead-instructions, 11#typeahead-instructions,
11#jump-to-results { 12my-suggestions ::ng-deep ul {
12 border: 1px solid var(--mainBackgroundColor); 13 border: 1px solid var(--mainBackgroundColor);
13 border-bottom-right-radius: 3px; 14 border-bottom-right-radius: 3px;
14 border-bottom-left-radius: 3px; 15 border-bottom-left-radius: 3px;
@@ -17,10 +18,12 @@
17 transition-property: box-shadow; 18 transition-property: box-shadow;
18} 19}
19 20
21#typeahead-help,
20#typeahead-instructions { 22#typeahead-instructions {
21 margin-top: 10px; 23 margin-top: 10px;
22 width: 100%; 24 width: 100%;
23 padding: .5rem 1rem; 25 padding: .5rem 1rem;
26 white-space: normal;
24 27
25 ul { 28 ul {
26 list-style: none; 29 list-style: none;
@@ -58,8 +61,9 @@
58 & > div:last-child { 61 & > div:last-child {
59 display: initial !important; 62 display: initial !important;
60 63
64 #typeahead-help,
61 #typeahead-instructions, 65 #typeahead-instructions,
62 #jump-to-results { 66 my-suggestions ::ng-deep ul {
63 box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px; 67 box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px;
64 } 68 }
65 } 69 }
@@ -76,33 +80,17 @@
76 } 80 }
77} 81}
78 82
79a.focus-visible {
80 background-color: var(--mainHoverColor);
81}
82
83a {
84 @include disable-default-a-behaviour;
85 width: 100%;
86
87 &, &:hover {
88 color: var(--mainForegroundColor);
89 }
90}
91
92.bg-gray {
93 background-color: var(--mainBackgroundColor);
94}
95
96.text-gray-light {
97 color: var(--mainForegroundColor);
98}
99
100.glyphicon { 83.glyphicon {
101 top: 3px; 84 top: 3px;
102} 85}
103 86
104.advanced-search-status { 87.advanced-search-status {
105 cursor: help; 88 height: max-content;
89 cursor: default;
90
91 &.c-help {
92 cursor: help;
93 }
106} 94}
107 95
108.small-title { 96.small-title {
@@ -111,11 +99,6 @@ a {
111 margin-bottom: .5rem; 99 margin-bottom: .5rem;
112} 100}
113 101
114my-global-icon { 102::ng-deep my-suggestion {
115 width: 17px; 103 width: 100%;
116 position: relative;
117 top: -2px;
118 margin: 5px;
119
120 @include apply-svg-color(var(--mainForegroundColor))
121} 104}
diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts
index d12a9682e..084bdd58b 100644
--- a/client/src/app/header/search-typeahead.component.ts
+++ b/client/src/app/header/search-typeahead.component.ts
@@ -1,23 +1,31 @@
1import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core' 1import {
2 Component,
3 ViewChild,
4 ElementRef,
5 AfterViewInit,
6 OnInit,
7 OnDestroy,
8 QueryList
9} from '@angular/core'
2import { Router, NavigationEnd } from '@angular/router' 10import { Router, NavigationEnd } from '@angular/router'
3import { AuthService } from '@app/core' 11import { AuthService } from '@app/core'
4import { I18n } from '@ngx-translate/i18n-polyfill' 12import { I18n } from '@ngx-translate/i18n-polyfill'
5import { filter } from 'rxjs/operators' 13import { filter } from 'rxjs/operators'
6import { ListKeyManager, ListKeyManagerOption } from '@angular/cdk/a11y' 14import { ListKeyManager } from '@angular/cdk/a11y'
7import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes' 15import { UP_ARROW, DOWN_ARROW, ENTER, TAB } from '@angular/cdk/keycodes'
16import { SuggestionComponent } from './suggestion.component'
8 17
9@Component({ 18@Component({
10 selector: 'my-search-typeahead', 19 selector: 'my-search-typeahead',
11 templateUrl: './search-typeahead.component.html', 20 templateUrl: './search-typeahead.component.html',
12 styleUrls: [ './search-typeahead.component.scss' ] 21 styleUrls: [ './search-typeahead.component.scss' ]
13}) 22})
14export class SearchTypeaheadComponent implements OnInit, AfterViewInit { 23export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit {
15 @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef 24 @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef
16 @ViewChild('optionsList', { static: true }) optionsList: ElementRef
17 25
18 hasChannel = false 26 hasChannel = false
19 inChannel = false 27 inChannel = false
20 keyboardEventsManager: ListKeyManager<ListKeyManagerOption> 28 newSearch = true
21 29
22 searchInput: HTMLInputElement 30 searchInput: HTMLInputElement
23 URIPolicy: 'only-followed' | 'any' = 'any' 31 URIPolicy: 'only-followed' | 'any' = 'any'
@@ -25,7 +33,9 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
25 URIPolicyText: string 33 URIPolicyText: string
26 inAllText: string 34 inAllText: string
27 inThisChannelText: string 35 inThisChannelText: string
36 globalSearchIndex = 'https://index.joinpeertube.org'
28 37
38 keyboardEventsManager: ListKeyManager<SuggestionComponent>
29 results: any[] = [] 39 results: any[] = []
30 40
31 constructor ( 41 constructor (
@@ -33,7 +43,7 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
33 private router: Router, 43 private router: Router,
34 private i18n: I18n 44 private i18n: I18n
35 ) { 45 ) {
36 this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content from its URL, or if your instance only allows doing so for instances it follows.') 46 this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, or if your instance only allows doing so for instances it follows.')
37 this.inAllText = this.i18n('In all PeerTube') 47 this.inAllText = this.i18n('In all PeerTube')
38 this.inThisChannelText = this.i18n('In this channel') 48 this.inThisChannelText = this.i18n('In this channel')
39 } 49 }
@@ -48,16 +58,30 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
48 }) 58 })
49 } 59 }
50 60
61 ngOnDestroy () {
62 if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
63 }
64
51 ngAfterViewInit () { 65 ngAfterViewInit () {
52 this.searchInput = this.contentWrapper.nativeElement.childNodes[0] 66 this.searchInput = this.contentWrapper.nativeElement.childNodes[0]
53 this.searchInput.addEventListener('input', this.computeResults.bind(this)) 67 this.searchInput.addEventListener('input', this.computeResults.bind(this))
68 this.searchInput.addEventListener('keyup', this.handleKeyUp.bind(this))
54 } 69 }
55 70
56 get hasSearch () { 71 get hasSearch () {
57 return !!this.searchInput && !!this.searchInput.value 72 return !!this.searchInput && !!this.searchInput.value
58 } 73 }
59 74
75 get activeResult () {
76 return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result
77 }
78
79 get showHelp () {
80 return this.hasSearch && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false
81 }
82
60 computeResults () { 83 computeResults () {
84 this.newSearch = true
61 let results = [ 85 let results = [
62 { 86 {
63 text: 'Maître poney', 87 text: 'Maître poney',
@@ -73,6 +97,10 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
73 }, 97 },
74 { 98 {
75 text: this.searchInput.value, 99 text: this.searchInput.value,
100 type: 'search-instance'
101 },
102 {
103 text: this.searchInput.value,
76 type: 'search-global' 104 type: 'search-global'
77 }, 105 },
78 ...results 106 ...results
@@ -90,20 +118,38 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
90 ) 118 )
91 } 119 }
92 120
121 initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) {
122 if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
123 this.keyboardEventsManager = new ListKeyManager(event.items)
124 if (event.index !== undefined) {
125 this.keyboardEventsManager.setActiveItem(event.index)
126 event.items.forEach(e => e.active = false)
127 this.keyboardEventsManager.activeItem.active = true
128 }
129 this.keyboardEventsManager.change.subscribe(
130 val => {
131 event.items.forEach(e => e.active = false)
132 this.keyboardEventsManager.activeItem.active = true
133 }
134 )
135 }
136
93 isUserLoggedIn () { 137 isUserLoggedIn () {
94 return this.authService.isLoggedIn() 138 return this.authService.isLoggedIn()
95 } 139 }
96 140
97 handleKeyUp (event: KeyboardEvent) { 141 handleKeyUp (event: KeyboardEvent, indexSelected?: number) {
98 event.stopImmediatePropagation() 142 event.stopImmediatePropagation()
99 if (this.keyboardEventsManager) { 143 if (this.keyboardEventsManager) {
100 if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) { 144 if (event.keyCode === TAB) {
101 // passing the event to key manager so we get a change fired 145 this.keyboardEventsManager.setNextItemActive()
146 return false
147 } else if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
102 this.keyboardEventsManager.onKeydown(event) 148 this.keyboardEventsManager.onKeydown(event)
103 return false 149 return false
104 } else if (event.keyCode === ENTER) { 150 } else if (event.keyCode === ENTER) {
105 // when we hit enter, the keyboardManager should call the selectItem method of the `ListItemComponent` 151 this.newSearch = false
106 // this.keyboardEventsManager.activeItem 152 // this.router.navigate(this.keyboardEventsManager.activeItem.result)
107 return false 153 return false
108 } 154 }
109 } 155 }
diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html
new file mode 100644
index 000000000..894cacb95
--- /dev/null
+++ b/client/src/app/header/suggestion.component.html
@@ -0,0 +1,28 @@
1<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active" [routerLink]="result.routerLink">
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'" [attr.aria-label]="inThisChannelText">
13 {{ inThisChannelText }}
14 </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>
22 </div>
23
24 <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>
25 Jump to channel
26 <span class="d-inline-block ml-1 v-align-middle">↵</span>
27 </div>
28</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..75c44a583
--- /dev/null
+++ b/client/src/app/header/suggestion.component.ts
@@ -0,0 +1,48 @@
1import { Input, Component, Output, EventEmitter, OnInit } from '@angular/core'
2import { RouterLink } from '@angular/router'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { ListKeyManagerOption } from '@angular/cdk/a11y'
5
6type Result = {
7 text: string
8 type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any'
9 routerLink?: RouterLink
10}
11
12@Component({
13 selector: 'my-suggestion',
14 templateUrl: './suggestion.component.html',
15 styleUrls: [ './suggestion.component.scss' ]
16})
17export class SuggestionComponent implements OnInit, ListKeyManagerOption {
18 @Input() result: Result
19 @Input() highlight: string
20 @Output() selected = new EventEmitter()
21
22 inAllText: string
23 inThisChannelText: string
24 inThisInstanceText: string
25
26 disabled = false
27 active = false
28
29 constructor (
30 private i18n: I18n
31 ) {
32 this.inAllText = this.i18n('In the vidiverse')
33 this.inThisChannelText = this.i18n('In this channel')
34 this.inThisInstanceText = this.i18n('In this instance')
35 }
36
37 getLabel () {
38 return this.result.text
39 }
40
41 ngOnInit () {
42 this.active = false
43 }
44
45 selectItem () {
46 this.selected.emit(this.result)
47 }
48}
diff --git a/client/src/app/header/suggestions.component.ts b/client/src/app/header/suggestions.component.ts
new file mode 100644
index 000000000..122c09388
--- /dev/null
+++ b/client/src/app/header/suggestions.component.ts
@@ -0,0 +1,31 @@
1import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren } from '@angular/core'
2import { SuggestionComponent } from './suggestion.component'
3
4@Component({
5 selector: 'my-suggestions',
6 template: `
7 <ul role="listbox" class="p-0 m-0">
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})
15export class SuggestionsComponent implements AfterViewInit {
16 @Input() results: any[]
17 @Input() highlight: string
18 @ViewChildren(SuggestionComponent) listItems: QueryList<SuggestionComponent>
19 @Output() init = new EventEmitter()
20
21 ngAfterViewInit () {
22 this.init.emit({ items: this.listItems })
23 this.listItems.changes.subscribe(
24 val => this.init.emit({ items: this.listItems })
25 )
26 }
27
28 hoverItem (index: number) {
29 this.init.emit({ items: this.listItems, index: index })
30 }
31}