1
0
Fork 0

Gracefully downsize search bar for mobile devices

This commit is contained in:
Rigel Kent 2020-02-03 14:04:42 +01:00
parent 6af662a596
commit 52cc0d5485
No known key found for this signature in database
GPG Key ID: 5E53E96A494E452F
10 changed files with 93 additions and 86 deletions

View File

@ -15,7 +15,7 @@
</div> </div>
<div class="header-right" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }"> <div class="header-right" [ngClass]="{ 'border-bottom': isMenuDisplayed === false }">
<my-header></my-header> <my-header class="w-100 d-flex justify-content-end"></my-header>
</div> </div>
</div> </div>

View File

@ -1,9 +1,9 @@
<my-search-typeahead> <my-search-typeahead class="w-100 d-flex justify-content-end">
<input <input
type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search locally videos, channels…" type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch"
[(ngModel)]="searchValue" (keyup.enter)="doSearch()" i18n-placeholder placeholder="Search videos, channels… known by this instance" [(ngModel)]="searchValue"
> >
<span (click)="doSearch()" class="icon icon-search"></span> <span class="icon icon-search"></span>
</my-search-typeahead> </my-search-typeahead>
<a class="upload-button" routerLink="/videos/upload"> <a class="upload-button" routerLink="/videos/upload">

View File

@ -14,14 +14,6 @@ my-search-typeahead {
&::placeholder { &::placeholder {
color: var(--inputPlaceholderColor); color: var(--inputPlaceholderColor);
} }
@media screen and (max-width: 800px) {
width: calc(100% - 150px);
}
@media screen and (max-width: 600px) {
width: calc(100% - 70px);
}
} }
.icon.icon-search { .icon.icon-search {
@ -45,10 +37,6 @@ my-search-typeahead {
color: var(--mainBackgroundColor) !important; color: var(--mainBackgroundColor) !important;
margin-right: 25px; margin-right: 25px;
@media screen and (max-width: 800px) {
margin-right: 0;
}
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
margin-right: 10px; margin-right: 10px;
padding: 0 10px; padding: 0 10px;

View File

@ -1,9 +1,4 @@
import { filter, first, map, tap } from 'rxjs/operators'
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router'
import { getParameterByName } from '../shared/misc/utils'
import { AuthService } from '@app/core'
import { of } from 'rxjs'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
@Component({ @Component({
@ -17,46 +12,10 @@ export class HeaderComponent implements OnInit {
ariaLabelTextForSearch = '' ariaLabelTextForSearch = ''
constructor ( constructor (
private router: Router,
private route: ActivatedRoute,
private auth: AuthService,
private i18n: I18n private i18n: I18n
) {} ) {}
ngOnInit () { ngOnInit () {
this.ariaLabelTextForSearch = this.i18n('Search videos, channels') this.ariaLabelTextForSearch = this.i18n('Search videos, channels')
this.router.events
.pipe(
filter(e => e instanceof NavigationEnd),
map(() => getParameterByName('search', window.location.href))
)
.subscribe(searchQuery => this.searchValue = searchQuery || '')
}
doSearch () {
const queryParams: Params = {}
if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
Object.assign(queryParams, this.route.snapshot.queryParams)
}
Object.assign(queryParams, { search: this.searchValue })
const o = this.auth.isLoggedIn()
? this.loadUserLanguagesIfNeeded(queryParams)
: of(true)
o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
}
private loadUserLanguagesIfNeeded (queryParams: any) {
if (queryParams && queryParams.languageOneOf) return of(queryParams)
return this.auth.userInformationLoaded
.pipe(
first(),
tap(() => Object.assign(queryParams, { languageOneOf: this.auth.getUser().videoLanguages }))
)
} }
} }

View File

@ -46,6 +46,18 @@ my-suggestions ::ng-deep ul {
transition: box-shadow .3s ease, width .2s ease; transition: box-shadow .3s ease, width .2s ease;
} }
@media screen and (min-width: 500px) {
margin-left: 10px;
}
@media screen and (max-width: 800px) {
flex: 1;
::ng-deep input {
width: unset;
}
}
::ng-deep span { ::ng-deep span {
right: 10px; right: 10px;
} }
@ -59,7 +71,9 @@ my-suggestions ::ng-deep ul {
&:focus, &:focus,
::ng-deep &:focus-within { ::ng-deep &:focus-within {
& > div:last-child { & > div:last-child {
@media screen and (min-width: 500px) {
display: initial !important; display: initial !important;
}
#typeahead-help, #typeahead-help,
#typeahead-instructions, #typeahead-instructions,

View File

@ -7,13 +7,15 @@ import {
OnDestroy, OnDestroy,
QueryList QueryList
} from '@angular/core' } from '@angular/core'
import { Router, NavigationEnd } from '@angular/router' import { Router, NavigationEnd, Params, ActivatedRoute } from '@angular/router'
import { AuthService } from '@app/core' import { AuthService } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { filter } from 'rxjs/operators' import { filter, first, tap, map } from 'rxjs/operators'
import { ListKeyManager } from '@angular/cdk/a11y' import { ListKeyManager } from '@angular/cdk/a11y'
import { UP_ARROW, DOWN_ARROW, ENTER, TAB } from '@angular/cdk/keycodes' import { UP_ARROW, DOWN_ARROW, ENTER, TAB } from '@angular/cdk/keycodes'
import { SuggestionComponent } from './suggestion.component' import { SuggestionComponent, Result } from './suggestion.component'
import { of } from 'rxjs'
import { getParameterByName } from '@app/shared/misc/utils'
@Component({ @Component({
selector: 'my-search-typeahead', selector: 'my-search-typeahead',
@ -41,6 +43,7 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
constructor ( constructor (
private authService: AuthService, private authService: AuthService,
private router: Router, private router: Router,
private route: ActivatedRoute,
private i18n: I18n private i18n: I18n
) { ) {
this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, or if your instance only allows doing so for instances it follows.') 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
ngOnInit () { ngOnInit () {
this.router.events this.router.events
.pipe(filter(event => event instanceof NavigationEnd)) .pipe(filter(e => e instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => { .subscribe((event: NavigationEnd) => {
this.hasChannel = event.url.startsWith('/videos/watch') this.hasChannel = event.url.startsWith('/videos/watch')
this.inChannel = event.url.startsWith('/video-channels') this.inChannel = event.url.startsWith('/video-channels')
this.computeResults() this.computeResults()
}) })
this.router.events
.pipe(
filter(e => e instanceof NavigationEnd),
map(() => getParameterByName('search', window.location.href))
)
.subscribe(searchQuery => this.searchInput.value = searchQuery || '')
} }
ngOnDestroy () { ngOnDestroy () {
@ -82,33 +92,33 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
computeResults () { computeResults () {
this.newSearch = true this.newSearch = true
let results = [ let results: Result[] = []
{
text: 'Maître poney',
type: 'channel'
}
]
if (this.hasSearch) { if (this.hasSearch) {
results = [ results = [
/* Channel search is still unimplemented. Uncomment when it is.
{ {
text: this.searchInput.value, text: this.searchInput.value,
type: 'search-channel' type: 'search-channel'
}, },
*/
{ {
text: this.searchInput.value, text: this.searchInput.value,
type: 'search-instance' type: 'search-instance',
default: true
}, },
/* Global search is still unimplemented. Uncomment when it is.
{ {
text: this.searchInput.value, text: this.searchInput.value,
type: 'search-global' type: 'search-global'
}, },
*/
...results ...results
] ]
} }
this.results = results.filter( this.results = results.filter(
result => { (result: Result) => {
// if we're not in a channel or one of its videos/playlits, show all channel-related results // if we're not in a channel or one of its videos/playlits, show all channel-related results
if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel')
// if we're in a channel, show all channel-related results except for the channel redirection itself // 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
) )
} }
setEventItems (event: { items: QueryList<SuggestionComponent>, index?: number }) {
event.items.forEach(e => {
if (this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem === e) {
this.keyboardEventsManager.activeItem.active = true
} else {
e.active = false
}
})
}
initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) { initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) {
if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe() if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
this.keyboardEventsManager = new ListKeyManager(event.items) this.keyboardEventsManager = new ListKeyManager(event.items)
if (event.index !== undefined) { if (event.index !== undefined) {
this.keyboardEventsManager.setActiveItem(event.index) this.keyboardEventsManager.setActiveItem(event.index)
event.items.forEach(e => e.active = false) } else {
this.keyboardEventsManager.activeItem.active = true this.keyboardEventsManager.setFirstItemActive()
} }
this.keyboardEventsManager.change.subscribe( this.keyboardEventsManager.change.subscribe(
val => { _ => this.setEventItems(event)
event.items.forEach(e => e.active = false)
this.keyboardEventsManager.activeItem.active = true
}
) )
} }
@ -141,17 +158,40 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
handleKeyUp (event: KeyboardEvent, indexSelected?: number) { handleKeyUp (event: KeyboardEvent, indexSelected?: number) {
event.stopImmediatePropagation() event.stopImmediatePropagation()
if (this.keyboardEventsManager) { if (this.keyboardEventsManager) {
if (event.keyCode === TAB) { if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
this.keyboardEventsManager.setNextItemActive()
return false
} else if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
this.keyboardEventsManager.onKeydown(event) this.keyboardEventsManager.onKeydown(event)
return false return false
} else if (event.keyCode === ENTER) { } else if (event.keyCode === ENTER) {
this.newSearch = false this.newSearch = false
// this.router.navigate(this.keyboardEventsManager.activeItem.result) this.doSearch()
return false return false
} }
} }
} }
doSearch () {
const queryParams: Params = {}
if (window.location.pathname === '/search' && this.route.snapshot.queryParams) {
Object.assign(queryParams, this.route.snapshot.queryParams)
}
Object.assign(queryParams, { search: this.searchInput.value })
const o = this.authService.isLoggedIn()
? this.loadUserLanguagesIfNeeded(queryParams)
: of(true)
o.subscribe(() => this.router.navigate([ '/search' ], { queryParams }))
}
private loadUserLanguagesIfNeeded (queryParams: any) {
if (queryParams && queryParams.languageOneOf) return of(queryParams)
return this.authService.userInformationLoaded
.pipe(
first(),
tap(() => Object.assign(queryParams, { languageOneOf: this.authService.getUser().videoLanguages }))
)
}
} }

View File

@ -3,10 +3,11 @@ import { RouterLink } from '@angular/router'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { ListKeyManagerOption } from '@angular/cdk/a11y' import { ListKeyManagerOption } from '@angular/cdk/a11y'
type Result = { export type Result = {
text: string text: string
type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any' type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any'
routerLink?: RouterLink routerLink?: RouterLink,
default?: boolean
} }
@Component({ @Component({
@ -39,7 +40,7 @@ export class SuggestionComponent implements OnInit, ListKeyManagerOption {
} }
ngOnInit () { ngOnInit () {
this.active = false if (this.result.default) this.active = true
} }
selectItem () { selectItem () {

View File

@ -19,7 +19,6 @@ export class SuggestionsComponent implements AfterViewInit {
@Output() init = new EventEmitter() @Output() init = new EventEmitter()
ngAfterViewInit () { ngAfterViewInit () {
this.init.emit({ items: this.listItems })
this.listItems.changes.subscribe( this.listItems.changes.subscribe(
val => this.init.emit({ items: this.listItems }) val => this.init.emit({ items: this.listItems })
) )

View File

@ -7,6 +7,7 @@
padding: 0; padding: 0;
width: $menu-width; width: $menu-width;
z-index: z(menu); z-index: z(menu);
scrollbar-color: var(--actionButtonColor) var(--menuBackgroundColor);
} }
menu { menu {

View File

@ -47,6 +47,11 @@ body {
font-size: 14px; font-size: 14px;
} }
::selection {
color: var(--mainBackgroundColor);
background-color: var(--mainHoverColor);
}
#incompatible-browser { #incompatible-browser {
display: none; display: none;
text-align: center; text-align: center;