diff options
Diffstat (limited to 'client/src/app/header')
-rw-r--r-- | client/src/app/header/index.ts | 1 | ||||
-rw-r--r-- | client/src/app/header/search-typeahead.component.html | 41 | ||||
-rw-r--r-- | client/src/app/header/search-typeahead.component.scss | 8 | ||||
-rw-r--r-- | client/src/app/header/search-typeahead.component.ts | 196 | ||||
-rw-r--r-- | client/src/app/header/suggestion.component.html | 21 | ||||
-rw-r--r-- | client/src/app/header/suggestion.component.ts | 22 | ||||
-rw-r--r-- | client/src/app/header/suggestions.component.html | 6 | ||||
-rw-r--r-- | client/src/app/header/suggestions.component.ts | 24 |
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 @@ | |||
1 | export * from './header.component' | 1 | export * from './header.component' |
2 | export * from './search-typeahead.component' | 2 | export * from './search-typeahead.component' |
3 | export * from './suggestions.component' | ||
4 | export * from './suggestion.component' | 3 | export * 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, |
39 | my-suggestions ::ng-deep ul { | 39 | li.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 @@ | |||
1 | import { Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild } from '@angular/core' | 1 | import { of } from 'rxjs' |
2 | import { first, tap, delay } from 'rxjs/operators' | ||
3 | import { ListKeyManager } from '@angular/cdk/a11y' | ||
4 | import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren, AfterViewChecked } from '@angular/core' | ||
2 | import { ActivatedRoute, Params, Router } from '@angular/router' | 5 | import { ActivatedRoute, Params, Router } from '@angular/router' |
3 | import { AuthService, ServerService } from '@app/core' | 6 | import { AuthService, ServerService } from '@app/core' |
4 | import { first, tap } from 'rxjs/operators' | ||
5 | import { ListKeyManager } from '@angular/cdk/a11y' | ||
6 | import { Result, SuggestionComponent } from './suggestion.component' | ||
7 | import { of } from 'rxjs' | ||
8 | import { ServerConfig } from '@shared/models' | 7 | import { ServerConfig } from '@shared/models' |
8 | import { SearchTargetType } from '@shared/models/search/search-target-query.model' | ||
9 | import { 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 | }) |
15 | export class SearchTypeaheadComponent implements OnInit, OnDestroy { | 16 | export 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 @@ | |||
1 | import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core' | 1 | import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy, OnChanges } from '@angular/core' |
2 | import { RouterLink } from '@angular/router' | 2 | import { RouterLink } from '@angular/router' |
3 | import { ListKeyManagerOption } from '@angular/cdk/a11y' | 3 | import { ListKeyManagerOption } from '@angular/cdk/a11y' |
4 | 4 | ||
5 | export type Result = { | 5 | export 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 | ||
12 | export 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 | }) |
18 | export class SuggestionComponent implements OnInit, ListKeyManagerOption { | 19 | export 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 @@ | |||
1 | import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core' | ||
2 | import { SuggestionComponent } from './suggestion.component' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-suggestions', | ||
6 | templateUrl: './suggestions.component.html', | ||
7 | changeDetection: ChangeDetectionStrategy.OnPush | ||
8 | }) | ||
9 | export 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 | } | ||