diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-02-03 14:04:42 +0100 |
---|---|---|
committer | Rigel Kent <sendmemail@rigelk.eu> | 2020-02-13 16:32:58 +0100 |
commit | 52cc0d54850e0acf069d2f95d063826f16ff5238 (patch) | |
tree | fb7450bc09fc5aa34c480bd197407c4881b85299 /client/src/app/header | |
parent | 6af662a5961b48ac12682df2b8b971060a2cc67d (diff) | |
download | PeerTube-52cc0d54850e0acf069d2f95d063826f16ff5238.tar.gz PeerTube-52cc0d54850e0acf069d2f95d063826f16ff5238.tar.zst PeerTube-52cc0d54850e0acf069d2f95d063826f16ff5238.zip |
Gracefully downsize search bar for mobile devices
Diffstat (limited to 'client/src/app/header')
-rw-r--r-- | client/src/app/header/header.component.html | 8 | ||||
-rw-r--r-- | client/src/app/header/header.component.scss | 12 | ||||
-rw-r--r-- | client/src/app/header/header.component.ts | 41 | ||||
-rw-r--r-- | client/src/app/header/search-typeahead.component.scss | 16 | ||||
-rw-r--r-- | client/src/app/header/search-typeahead.component.ts | 86 | ||||
-rw-r--r-- | client/src/app/header/suggestion.component.ts | 7 | ||||
-rw-r--r-- | client/src/app/header/suggestions.component.ts | 1 |
7 files changed, 86 insertions, 85 deletions
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index 074bebf21..561ee6c16 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html | |||
@@ -1,9 +1,9 @@ | |||
1 | <my-search-typeahead> | 1 | <my-search-typeahead class="w-100 d-flex justify-content-end"> |
2 | <input | 2 | <input |
3 | type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search locally videos, channels…" | 3 | type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" |
4 | [(ngModel)]="searchValue" (keyup.enter)="doSearch()" | 4 | i18n-placeholder placeholder="Search videos, channels… known by this instance" [(ngModel)]="searchValue" |
5 | > | 5 | > |
6 | <span (click)="doSearch()" class="icon icon-search"></span> | 6 | <span class="icon icon-search"></span> |
7 | </my-search-typeahead> | 7 | </my-search-typeahead> |
8 | 8 | ||
9 | <a class="upload-button" routerLink="/videos/upload"> | 9 | <a class="upload-button" routerLink="/videos/upload"> |
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss index b602cf0a8..2f0a407fc 100644 --- a/client/src/app/header/header.component.scss +++ b/client/src/app/header/header.component.scss | |||
@@ -14,14 +14,6 @@ my-search-typeahead { | |||
14 | &::placeholder { | 14 | &::placeholder { |
15 | color: var(--inputPlaceholderColor); | 15 | color: var(--inputPlaceholderColor); |
16 | } | 16 | } |
17 | |||
18 | @media screen and (max-width: 800px) { | ||
19 | width: calc(100% - 150px); | ||
20 | } | ||
21 | |||
22 | @media screen and (max-width: 600px) { | ||
23 | width: calc(100% - 70px); | ||
24 | } | ||
25 | } | 17 | } |
26 | 18 | ||
27 | .icon.icon-search { | 19 | .icon.icon-search { |
@@ -45,10 +37,6 @@ my-search-typeahead { | |||
45 | color: var(--mainBackgroundColor) !important; | 37 | color: var(--mainBackgroundColor) !important; |
46 | margin-right: 25px; | 38 | margin-right: 25px; |
47 | 39 | ||
48 | @media screen and (max-width: 800px) { | ||
49 | margin-right: 0; | ||
50 | } | ||
51 | |||
52 | @media screen and (max-width: 600px) { | 40 | @media screen and (max-width: 600px) { |
53 | margin-right: 10px; | 41 | margin-right: 10px; |
54 | padding: 0 10px; | 42 | padding: 0 10px; |
diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts index ca4a32cbc..d9311c554 100644 --- a/client/src/app/header/header.component.ts +++ b/client/src/app/header/header.component.ts | |||
@@ -1,9 +1,4 @@ | |||
1 | import { filter, first, map, tap } from 'rxjs/operators' | ||
2 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
3 | import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router' | ||
4 | import { getParameterByName } from '../shared/misc/utils' | ||
5 | import { AuthService } from '@app/core' | ||
6 | import { of } from 'rxjs' | ||
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 2 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | 3 | ||
9 | @Component({ | 4 | @Component({ |
@@ -17,46 +12,10 @@ export class HeaderComponent implements OnInit { | |||
17 | ariaLabelTextForSearch = '' | 12 | ariaLabelTextForSearch = '' |
18 | 13 | ||
19 | constructor ( | 14 | constructor ( |
20 | private router: Router, | ||
21 | private route: ActivatedRoute, | ||
22 | private auth: AuthService, | ||
23 | private i18n: I18n | 15 | private i18n: I18n |
24 | ) {} | 16 | ) {} |
25 | 17 | ||
26 | ngOnInit () { | 18 | ngOnInit () { |
27 | this.ariaLabelTextForSearch = this.i18n('Search videos, channels') | 19 | this.ariaLabelTextForSearch = this.i18n('Search videos, channels') |
28 | |||
29 | this.router.events | ||
30 | .pipe( | ||
31 | filter(e => e instanceof NavigationEnd), | ||
32 | map(() => getParameterByName('search', window.location.href)) | ||
33 | ) | ||
34 | .subscribe(searchQuery => this.searchValue = searchQuery || '') | ||
35 | } | ||
36 | |||
37 | doSearch () { | ||
38 | const queryParams: Params = {} | ||
39 | |||
40 | if (window.location.pathname === '/search' && this.route.snapshot.queryParams) { | ||
41 | Object.assign(queryParams, this.route.snapshot.queryParams) | ||
42 | } | ||
43 | |||
44 | Object.assign(queryParams, { search: this.searchValue }) | ||
45 | |||
46 | const o = this.auth.isLoggedIn() | ||
47 | ? this.loadUserLanguagesIfNeeded(queryParams) | ||
48 | : of(true) | ||
49 | |||
50 | o.subscribe(() => this.router.navigate([ '/search' ], { queryParams })) | ||
51 | } | ||
52 | |||
53 | private loadUserLanguagesIfNeeded (queryParams: any) { | ||
54 | if (queryParams && queryParams.languageOneOf) return of(queryParams) | ||
55 | |||
56 | return this.auth.userInformationLoaded | ||
57 | .pipe( | ||
58 | first(), | ||
59 | tap(() => Object.assign(queryParams, { languageOneOf: this.auth.getUser().videoLanguages })) | ||
60 | ) | ||
61 | } | 20 | } |
62 | } | 21 | } |
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss index c410d4734..c2f5a1828 100644 --- a/client/src/app/header/search-typeahead.component.scss +++ b/client/src/app/header/search-typeahead.component.scss | |||
@@ -46,6 +46,18 @@ my-suggestions ::ng-deep ul { | |||
46 | transition: box-shadow .3s ease, width .2s ease; | 46 | transition: box-shadow .3s ease, width .2s ease; |
47 | } | 47 | } |
48 | 48 | ||
49 | @media screen and (min-width: 500px) { | ||
50 | margin-left: 10px; | ||
51 | } | ||
52 | |||
53 | @media screen and (max-width: 800px) { | ||
54 | flex: 1; | ||
55 | |||
56 | ::ng-deep input { | ||
57 | width: unset; | ||
58 | } | ||
59 | } | ||
60 | |||
49 | ::ng-deep span { | 61 | ::ng-deep span { |
50 | right: 10px; | 62 | right: 10px; |
51 | } | 63 | } |
@@ -59,7 +71,9 @@ my-suggestions ::ng-deep ul { | |||
59 | &:focus, | 71 | &:focus, |
60 | ::ng-deep &:focus-within { | 72 | ::ng-deep &:focus-within { |
61 | & > div:last-child { | 73 | & > div:last-child { |
62 | display: initial !important; | 74 | @media screen and (min-width: 500px) { |
75 | display: initial !important; | ||
76 | } | ||
63 | 77 | ||
64 | #typeahead-help, | 78 | #typeahead-help, |
65 | #typeahead-instructions, | 79 | #typeahead-instructions, |
diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts index 084bdd58b..514c04704 100644 --- a/client/src/app/header/search-typeahead.component.ts +++ b/client/src/app/header/search-typeahead.component.ts | |||
@@ -7,13 +7,15 @@ import { | |||
7 | OnDestroy, | 7 | OnDestroy, |
8 | QueryList | 8 | QueryList |
9 | } from '@angular/core' | 9 | } from '@angular/core' |
10 | import { Router, NavigationEnd } from '@angular/router' | 10 | import { Router, NavigationEnd, Params, ActivatedRoute } from '@angular/router' |
11 | import { AuthService } from '@app/core' | 11 | import { AuthService } from '@app/core' |
12 | import { I18n } from '@ngx-translate/i18n-polyfill' | 12 | import { I18n } from '@ngx-translate/i18n-polyfill' |
13 | import { filter } from 'rxjs/operators' | 13 | import { filter, first, tap, map } from 'rxjs/operators' |
14 | import { ListKeyManager } from '@angular/cdk/a11y' | 14 | import { ListKeyManager } from '@angular/cdk/a11y' |
15 | import { UP_ARROW, DOWN_ARROW, ENTER, TAB } from '@angular/cdk/keycodes' | 15 | import { UP_ARROW, DOWN_ARROW, ENTER, TAB } from '@angular/cdk/keycodes' |
16 | import { SuggestionComponent } from './suggestion.component' | 16 | import { SuggestionComponent, Result } from './suggestion.component' |
17 | import { of } from 'rxjs' | ||
18 | import { getParameterByName } from '@app/shared/misc/utils' | ||
17 | 19 | ||
18 | @Component({ | 20 | @Component({ |
19 | selector: 'my-search-typeahead', | 21 | selector: 'my-search-typeahead', |
@@ -41,6 +43,7 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni | |||
41 | constructor ( | 43 | constructor ( |
42 | private authService: AuthService, | 44 | private authService: AuthService, |
43 | private router: Router, | 45 | private router: Router, |
46 | private route: ActivatedRoute, | ||
44 | private i18n: I18n | 47 | private i18n: I18n |
45 | ) { | 48 | ) { |
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.') | 49 | this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, or if your instance only allows doing so for instances it follows.') |
@@ -50,12 +53,19 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni | |||
50 | 53 | ||
51 | ngOnInit () { | 54 | ngOnInit () { |
52 | this.router.events | 55 | this.router.events |
53 | .pipe(filter(event => event instanceof NavigationEnd)) | 56 | .pipe(filter(e => e instanceof NavigationEnd)) |
54 | .subscribe((event: NavigationEnd) => { | 57 | .subscribe((event: NavigationEnd) => { |
55 | this.hasChannel = event.url.startsWith('/videos/watch') | 58 | this.hasChannel = event.url.startsWith('/videos/watch') |
56 | this.inChannel = event.url.startsWith('/video-channels') | 59 | this.inChannel = event.url.startsWith('/video-channels') |
57 | this.computeResults() | 60 | this.computeResults() |
58 | }) | 61 | }) |
62 | |||
63 | this.router.events | ||
64 | .pipe( | ||
65 | filter(e => e instanceof NavigationEnd), | ||
66 | map(() => getParameterByName('search', window.location.href)) | ||
67 | ) | ||
68 | .subscribe(searchQuery => this.searchInput.value = searchQuery || '') | ||
59 | } | 69 | } |
60 | 70 | ||
61 | ngOnDestroy () { | 71 | ngOnDestroy () { |
@@ -82,33 +92,33 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni | |||
82 | 92 | ||
83 | computeResults () { | 93 | computeResults () { |
84 | this.newSearch = true | 94 | this.newSearch = true |
85 | let results = [ | 95 | let results: Result[] = [] |
86 | { | ||
87 | text: 'Maître poney', | ||
88 | type: 'channel' | ||
89 | } | ||
90 | ] | ||
91 | 96 | ||
92 | if (this.hasSearch) { | 97 | if (this.hasSearch) { |
93 | results = [ | 98 | results = [ |
99 | /* Channel search is still unimplemented. Uncomment when it is. | ||
94 | { | 100 | { |
95 | text: this.searchInput.value, | 101 | text: this.searchInput.value, |
96 | type: 'search-channel' | 102 | type: 'search-channel' |
97 | }, | 103 | }, |
104 | */ | ||
98 | { | 105 | { |
99 | text: this.searchInput.value, | 106 | text: this.searchInput.value, |
100 | type: 'search-instance' | 107 | type: 'search-instance', |
108 | default: true | ||
101 | }, | 109 | }, |
110 | /* Global search is still unimplemented. Uncomment when it is. | ||
102 | { | 111 | { |
103 | text: this.searchInput.value, | 112 | text: this.searchInput.value, |
104 | type: 'search-global' | 113 | type: 'search-global' |
105 | }, | 114 | }, |
115 | */ | ||
106 | ...results | 116 | ...results |
107 | ] | 117 | ] |
108 | } | 118 | } |
109 | 119 | ||
110 | this.results = results.filter( | 120 | this.results = results.filter( |
111 | result => { | 121 | (result: Result) => { |
112 | // if we're not in a channel or one of its videos/playlits, show all channel-related results | 122 | // if we're not in a channel or one of its videos/playlits, show all channel-related results |
113 | if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') | 123 | if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') |
114 | // if we're in a channel, show all channel-related results except for the channel redirection itself | 124 | // if we're in a channel, show all channel-related results except for the channel redirection itself |
@@ -118,19 +128,26 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni | |||
118 | ) | 128 | ) |
119 | } | 129 | } |
120 | 130 | ||
131 | setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) { | ||
132 | event.items.forEach(e => { | ||
133 | if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) { | ||
134 | this.keyboardEventsManager.activeItem.active = true | ||
135 | } else { | ||
136 | e.active = false | ||
137 | } | ||
138 | }) | ||
139 | } | ||
140 | |||
121 | initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) { | 141 | initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) { |
122 | if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() | 142 | if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() |
123 | this.keyboardEventsManager = new ListKeyManager(event.items) | 143 | this.keyboardEventsManager = new ListKeyManager(event.items) |
124 | if (event.index !== undefined) { | 144 | if (event.index !== undefined) { |
125 | this.keyboardEventsManager.setActiveItem(event.index) | 145 | this.keyboardEventsManager.setActiveItem(event.index) |
126 | event.items.forEach(e => e.active = false) | 146 | } else { |
127 | this.keyboardEventsManager.activeItem.active = true | 147 | this.keyboardEventsManager.setFirstItemActive() |
128 | } | 148 | } |
129 | this.keyboardEventsManager.change.subscribe( | 149 | this.keyboardEventsManager.change.subscribe( |
130 | val => { | 150 | _ => this.setEventItems(event) |
131 | event.items.forEach(e => e.active = false) | ||
132 | this.keyboardEventsManager.activeItem.active = true | ||
133 | } | ||
134 | ) | 151 | ) |
135 | } | 152 | } |
136 | 153 | ||
@@ -141,17 +158,40 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni | |||
141 | handleKeyUp (event: KeyboardEvent, indexSelected?: number) { | 158 | handleKeyUp (event: KeyboardEvent, indexSelected?: number) { |
142 | event.stopImmediatePropagation() | 159 | event.stopImmediatePropagation() |
143 | if (this.keyboardEventsManager) { | 160 | if (this.keyboardEventsManager) { |
144 | if (event.keyCode === TAB) { | 161 | if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) { |
145 | this.keyboardEventsManager.setNextItemActive() | ||
146 | return false | ||
147 | } else if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) { | ||
148 | this.keyboardEventsManager.onKeydown(event) | 162 | this.keyboardEventsManager.onKeydown(event) |
149 | return false | 163 | return false |
150 | } else if (event.keyCode === ENTER) { | 164 | } else if (event.keyCode === ENTER) { |
151 | this.newSearch = false | 165 | this.newSearch = false |
152 | // this.router.navigate(this.keyboardEventsManager.activeItem.result) | 166 | this.doSearch() |
153 | return false | 167 | return false |
154 | } | 168 | } |
155 | } | 169 | } |
156 | } | 170 | } |
171 | |||
172 | doSearch () { | ||
173 | const queryParams: Params = {} | ||
174 | |||
175 | if (window.location.pathname === '/search' && this.route.snapshot.queryParams) { | ||
176 | Object.assign(queryParams, this.route.snapshot.queryParams) | ||
177 | } | ||
178 | |||
179 | Object.assign(queryParams, { search: this.searchInput.value }) | ||
180 | |||
181 | const o = this.authService.isLoggedIn() | ||
182 | ? this.loadUserLanguagesIfNeeded(queryParams) | ||
183 | : of(true) | ||
184 | |||
185 | o.subscribe(() => this.router.navigate([ '/search' ], { queryParams })) | ||
186 | } | ||
187 | |||
188 | private loadUserLanguagesIfNeeded (queryParams: any) { | ||
189 | if (queryParams && queryParams.languageOneOf) return of(queryParams) | ||
190 | |||
191 | return this.authService.userInformationLoaded | ||
192 | .pipe( | ||
193 | first(), | ||
194 | tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages })) | ||
195 | ) | ||
196 | } | ||
157 | } | 197 | } |
diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts index 75c44a583..bdcb3e03f 100644 --- a/client/src/app/header/suggestion.component.ts +++ b/client/src/app/header/suggestion.component.ts | |||
@@ -3,10 +3,11 @@ import { RouterLink } from '@angular/router' | |||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { ListKeyManagerOption } from '@angular/cdk/a11y' | 4 | import { ListKeyManagerOption } from '@angular/cdk/a11y' |
5 | 5 | ||
6 | type Result = { | 6 | export type Result = { |
7 | text: string | 7 | text: string |
8 | type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any' | 8 | type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any' |
9 | routerLink?: RouterLink | 9 | routerLink?: RouterLink, |
10 | default?: boolean | ||
10 | } | 11 | } |
11 | 12 | ||
12 | @Component({ | 13 | @Component({ |
@@ -39,7 +40,7 @@ export class SuggestionComponent implements OnInit, ListKeyManagerOption { | |||
39 | } | 40 | } |
40 | 41 | ||
41 | ngOnInit () { | 42 | ngOnInit () { |
42 | this.active = false | 43 | if (this.result.default) this.active = true |
43 | } | 44 | } |
44 | 45 | ||
45 | selectItem () { | 46 | selectItem () { |
diff --git a/client/src/app/header/suggestions.component.ts b/client/src/app/header/suggestions.component.ts index 122c09388..fac7fe2f9 100644 --- a/client/src/app/header/suggestions.component.ts +++ b/client/src/app/header/suggestions.component.ts | |||
@@ -19,7 +19,6 @@ export class SuggestionsComponent implements AfterViewInit { | |||
19 | @Output() init = new EventEmitter() | 19 | @Output() init = new EventEmitter() |
20 | 20 | ||
21 | ngAfterViewInit () { | 21 | ngAfterViewInit () { |
22 | this.init.emit({ items: this.listItems }) | ||
23 | this.listItems.changes.subscribe( | 22 | this.listItems.changes.subscribe( |
24 | val => this.init.emit({ items: this.listItems }) | 23 | val => this.init.emit({ items: this.listItems }) |
25 | ) | 24 | ) |