Gracefully downsize search bar for mobile devices
This commit is contained in:
parent
6af662a596
commit
52cc0d5485
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 }))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 }))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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 })
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue