Add keyboard navigation and hepler to typeahead
This commit is contained in:
parent
f409f0c3b9
commit
6af662a596
|
@ -9,7 +9,7 @@ import 'focus-visible'
|
||||||
import { AppRoutingModule } from './app-routing.module'
|
import { AppRoutingModule } from './app-routing.module'
|
||||||
import { AppComponent } from './app.component'
|
import { AppComponent } from './app.component'
|
||||||
import { CoreModule } from './core'
|
import { CoreModule } from './core'
|
||||||
import { HeaderComponent, SearchTypeaheadComponent } from './header'
|
import { HeaderComponent, SearchTypeaheadComponent, SuggestionsComponent, SuggestionComponent } from './header'
|
||||||
import { LoginModule } from './login'
|
import { LoginModule } from './login'
|
||||||
import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
|
import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
|
||||||
import { SharedModule } from './shared'
|
import { SharedModule } from './shared'
|
||||||
|
@ -42,6 +42,8 @@ export function metaFactory (serverService: ServerService): MetaLoader {
|
||||||
AvatarNotificationComponent,
|
AvatarNotificationComponent,
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
SearchTypeaheadComponent,
|
SearchTypeaheadComponent,
|
||||||
|
SuggestionsComponent,
|
||||||
|
SuggestionComponent,
|
||||||
|
|
||||||
WelcomeModalComponent,
|
WelcomeModalComponent,
|
||||||
InstanceConfigWarningModalComponent
|
InstanceConfigWarningModalComponent
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<my-search-typeahead>
|
<my-search-typeahead>
|
||||||
<input
|
<input
|
||||||
type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search videos, channels…"
|
type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search locally videos, channels…"
|
||||||
[(ngModel)]="searchValue" (keyup.enter)="doSearch()"
|
[(ngModel)]="searchValue" (keyup.enter)="doSearch()"
|
||||||
>
|
>
|
||||||
<span (click)="doSearch()" class="icon icon-search"></span>
|
<span (click)="doSearch()" class="icon icon-search"></span>
|
||||||
|
|
|
@ -2,7 +2,7 @@ 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 { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router'
|
||||||
import { getParameterByName } from '../shared/misc/utils'
|
import { getParameterByName } from '../shared/misc/utils'
|
||||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
import { AuthService } from '@app/core'
|
||||||
import { of } from 'rxjs'
|
import { of } from 'rxjs'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
|
||||||
|
@ -20,9 +20,6 @@ export class HeaderComponent implements OnInit {
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private auth: AuthService,
|
private auth: AuthService,
|
||||||
private serverService: ServerService,
|
|
||||||
private authService: AuthService,
|
|
||||||
private notifier: Notifier,
|
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
export * from './header.component'
|
export * from './header.component'
|
||||||
export * from './search-typeahead.component'
|
export * from './search-typeahead.component'
|
||||||
|
export * from './suggestions.component'
|
||||||
|
export * from './suggestion.component'
|
||||||
|
|
|
@ -3,17 +3,27 @@
|
||||||
|
|
||||||
<div class="position-absolute jump-to-suggestions">
|
<div class="position-absolute jump-to-suggestions">
|
||||||
<!-- suggestions -->
|
<!-- suggestions -->
|
||||||
<ul id="jump-to-results" role="listbox" class="p-0 m-0" #optionsList>
|
<my-suggestions *ngIf="hasSearch && newSearch" [results]="results" [highlight]="searchInput.value" (init)="initKeyboardEventsManager($event)" #suggestions></my-suggestions>
|
||||||
<li *ngFor="let res of results" class="d-flex flex-justify-start flex-items-center p-0 f5" role="option" aria-selected="true">
|
|
||||||
<ng-container *ngTemplateOutlet="result; context: {$implicit: res}"></ng-container>
|
<!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
|
||||||
</li>
|
<div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden">
|
||||||
</ul>
|
<ng-container *ngIf="activeResult.type === 'search-global'">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<label class="small-title" i18n>Global search</label>
|
||||||
|
<div class="advanced-search-status text-muted">
|
||||||
|
<span class="mr-1" i18n>using {{ globalSearchIndex }}</span>
|
||||||
|
<i class="glyphicon glyphicon-globe"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- search instructions, when search input is empty -->
|
<!-- search instructions, when search input is empty -->
|
||||||
<div *ngIf="!hasSearch" id="typeahead-instructions" class="overflow-hidden">
|
<div *ngIf="!hasSearch" id="typeahead-instructions" class="overflow-hidden">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<label class="small-title" i18n>Advanced search</label>
|
<label class="small-title" i18n>Advanced search</label>
|
||||||
<div class="advanced-search-status">
|
<div class="advanced-search-status c-help">
|
||||||
<span [ngClass]="URIPolicy === 'any' ? 'text-success' : 'text-muted'" [title]="URIPolicyText">
|
<span [ngClass]="URIPolicy === 'any' ? 'text-success' : 'text-muted'" [title]="URIPolicyText">
|
||||||
<span class="mr-1" i18n>{URIPolicy, select, only-followed {only followed instances} other {any instance}} </span>
|
<span class="mr-1" i18n>{URIPolicy, select, only-followed {only followed instances} other {any instance}} </span>
|
||||||
<i [ngClass]="URIPolicy === 'any' ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
|
<i [ngClass]="URIPolicy === 'any' ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
|
||||||
|
@ -36,34 +46,3 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #result let-result>
|
|
||||||
<a tabindex="0" class="d-flex flex-auto flex-items-center p-2"
|
|
||||||
data-target-type="Repository"
|
|
||||||
[routerLink]="result.routerLink"
|
|
||||||
>
|
|
||||||
<div class="flex-shrink-0 mr-2 text-center">
|
|
||||||
<my-global-icon *ngIf="result.type !== 'channel'" iconName="search"></my-global-icon>
|
|
||||||
<my-global-icon *ngIf="result.type === 'channel'" iconName="folder"></my-global-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28">
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6">
|
|
||||||
<span *ngIf="result.type === 'search-channel'" [attr.aria-label]="inThisChannelText">
|
|
||||||
{{ inThisChannelText }}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="result.type === 'search-global'" [attr.aria-label]="inAllText">
|
|
||||||
{{ inAllText }}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
Jump to channel
|
|
||||||
<span class="d-inline-block ml-1 v-align-middle">↵</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</ng-template>
|
|
|
@ -7,8 +7,9 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#typeahead-help,
|
||||||
#typeahead-instructions,
|
#typeahead-instructions,
|
||||||
#jump-to-results {
|
my-suggestions ::ng-deep ul {
|
||||||
border: 1px solid var(--mainBackgroundColor);
|
border: 1px solid var(--mainBackgroundColor);
|
||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 3px;
|
||||||
border-bottom-left-radius: 3px;
|
border-bottom-left-radius: 3px;
|
||||||
|
@ -17,10 +18,12 @@
|
||||||
transition-property: box-shadow;
|
transition-property: box-shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#typeahead-help,
|
||||||
#typeahead-instructions {
|
#typeahead-instructions {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: .5rem 1rem;
|
padding: .5rem 1rem;
|
||||||
|
white-space: normal;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
@ -58,8 +61,9 @@
|
||||||
& > div:last-child {
|
& > div:last-child {
|
||||||
display: initial !important;
|
display: initial !important;
|
||||||
|
|
||||||
|
#typeahead-help,
|
||||||
#typeahead-instructions,
|
#typeahead-instructions,
|
||||||
#jump-to-results {
|
my-suggestions ::ng-deep ul {
|
||||||
box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px;
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,33 +80,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.focus-visible {
|
|
||||||
background-color: var(--mainHoverColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
@include disable-default-a-behaviour;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&, &:hover {
|
|
||||||
color: var(--mainForegroundColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-gray {
|
|
||||||
background-color: var(--mainBackgroundColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-gray-light {
|
|
||||||
color: var(--mainForegroundColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glyphicon {
|
.glyphicon {
|
||||||
top: 3px;
|
top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.advanced-search-status {
|
.advanced-search-status {
|
||||||
cursor: help;
|
height: max-content;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
&.c-help {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-title {
|
.small-title {
|
||||||
|
@ -111,11 +99,6 @@ a {
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
my-global-icon {
|
::ng-deep my-suggestion {
|
||||||
width: 17px;
|
width: 100%;
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
margin: 5px;
|
|
||||||
|
|
||||||
@include apply-svg-color(var(--mainForegroundColor))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,31 @@
|
||||||
import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core'
|
import {
|
||||||
|
Component,
|
||||||
|
ViewChild,
|
||||||
|
ElementRef,
|
||||||
|
AfterViewInit,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
|
QueryList
|
||||||
|
} from '@angular/core'
|
||||||
import { Router, NavigationEnd } from '@angular/router'
|
import { Router, NavigationEnd } 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 } from 'rxjs/operators'
|
||||||
import { ListKeyManager, ListKeyManagerOption } from '@angular/cdk/a11y'
|
import { ListKeyManager } from '@angular/cdk/a11y'
|
||||||
import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'
|
import { UP_ARROW, DOWN_ARROW, ENTER, TAB } from '@angular/cdk/keycodes'
|
||||||
|
import { SuggestionComponent } from './suggestion.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-search-typeahead',
|
selector: 'my-search-typeahead',
|
||||||
templateUrl: './search-typeahead.component.html',
|
templateUrl: './search-typeahead.component.html',
|
||||||
styleUrls: [ './search-typeahead.component.scss' ]
|
styleUrls: [ './search-typeahead.component.scss' ]
|
||||||
})
|
})
|
||||||
export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
|
export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||||
@ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef
|
@ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef
|
||||||
@ViewChild('optionsList', { static: true }) optionsList: ElementRef
|
|
||||||
|
|
||||||
hasChannel = false
|
hasChannel = false
|
||||||
inChannel = false
|
inChannel = false
|
||||||
keyboardEventsManager: ListKeyManager<ListKeyManagerOption>
|
newSearch = true
|
||||||
|
|
||||||
searchInput: HTMLInputElement
|
searchInput: HTMLInputElement
|
||||||
URIPolicy: 'only-followed' | 'any' = 'any'
|
URIPolicy: 'only-followed' | 'any' = 'any'
|
||||||
|
@ -25,7 +33,9 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
|
||||||
URIPolicyText: string
|
URIPolicyText: string
|
||||||
inAllText: string
|
inAllText: string
|
||||||
inThisChannelText: string
|
inThisChannelText: string
|
||||||
|
globalSearchIndex = 'https://index.joinpeertube.org'
|
||||||
|
|
||||||
|
keyboardEventsManager: ListKeyManager<SuggestionComponent>
|
||||||
results: any[] = []
|
results: any[] = []
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -33,7 +43,7 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
) {
|
) {
|
||||||
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.')
|
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.inAllText = this.i18n('In all PeerTube')
|
this.inAllText = this.i18n('In all PeerTube')
|
||||||
this.inThisChannelText = this.i18n('In this channel')
|
this.inThisChannelText = this.i18n('In this channel')
|
||||||
}
|
}
|
||||||
|
@ -48,16 +58,30 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewInit () {
|
ngAfterViewInit () {
|
||||||
this.searchInput = this.contentWrapper.nativeElement.childNodes[0]
|
this.searchInput = this.contentWrapper.nativeElement.childNodes[0]
|
||||||
this.searchInput.addEventListener('input', this.computeResults.bind(this))
|
this.searchInput.addEventListener('input', this.computeResults.bind(this))
|
||||||
|
this.searchInput.addEventListener('keyup', this.handleKeyUp.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasSearch () {
|
get hasSearch () {
|
||||||
return !!this.searchInput && !!this.searchInput.value
|
return !!this.searchInput && !!this.searchInput.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get activeResult () {
|
||||||
|
return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result
|
||||||
|
}
|
||||||
|
|
||||||
|
get showHelp () {
|
||||||
|
return this.hasSearch && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false
|
||||||
|
}
|
||||||
|
|
||||||
computeResults () {
|
computeResults () {
|
||||||
|
this.newSearch = true
|
||||||
let results = [
|
let results = [
|
||||||
{
|
{
|
||||||
text: 'Maître poney',
|
text: 'Maître poney',
|
||||||
|
@ -71,6 +95,10 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
|
||||||
text: this.searchInput.value,
|
text: this.searchInput.value,
|
||||||
type: 'search-channel'
|
type: 'search-channel'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.searchInput.value,
|
||||||
|
type: 'search-instance'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.searchInput.value,
|
text: this.searchInput.value,
|
||||||
type: 'search-global'
|
type: 'search-global'
|
||||||
|
@ -90,20 +118,38 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initKeyboardEventsManager (event: { items: QueryList<SuggestionComponent>, index?: number }) {
|
||||||
|
if (this.keyboardEventsManager) this.keyboardEventsManager.change.unsubscribe()
|
||||||
|
this.keyboardEventsManager = new ListKeyManager(event.items)
|
||||||
|
if (event.index !== undefined) {
|
||||||
|
this.keyboardEventsManager.setActiveItem(event.index)
|
||||||
|
event.items.forEach(e => e.active = false)
|
||||||
|
this.keyboardEventsManager.activeItem.active = true
|
||||||
|
}
|
||||||
|
this.keyboardEventsManager.change.subscribe(
|
||||||
|
val => {
|
||||||
|
event.items.forEach(e => e.active = false)
|
||||||
|
this.keyboardEventsManager.activeItem.active = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
isUserLoggedIn () {
|
isUserLoggedIn () {
|
||||||
return this.authService.isLoggedIn()
|
return this.authService.isLoggedIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyUp (event: KeyboardEvent) {
|
handleKeyUp (event: KeyboardEvent, indexSelected?: number) {
|
||||||
event.stopImmediatePropagation()
|
event.stopImmediatePropagation()
|
||||||
if (this.keyboardEventsManager) {
|
if (this.keyboardEventsManager) {
|
||||||
if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
|
if (event.keyCode === TAB) {
|
||||||
// passing the event to key manager so we get a change fired
|
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) {
|
||||||
// when we hit enter, the keyboardManager should call the selectItem method of the `ListItemComponent`
|
this.newSearch = false
|
||||||
// this.keyboardEventsManager.activeItem
|
// this.router.navigate(this.keyboardEventsManager.activeItem.result)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<a tabindex="-1" class="d-flex flex-auto flex-items-center p-2" [class.focus-visible]="active" [routerLink]="result.routerLink">
|
||||||
|
<div class="flex-shrink-0 mr-2 text-center">
|
||||||
|
<my-global-icon *ngIf="result.type !== 'channel'" iconName="search"></my-global-icon>
|
||||||
|
<my-global-icon *ngIf="result.type === 'channel'" iconName="folder"></my-global-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6">
|
||||||
|
<span *ngIf="result.type === 'search-channel'" [attr.aria-label]="inThisChannelText">
|
||||||
|
{{ inThisChannelText }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="result.type === 'search-instance'" [attr.aria-label]="inThisInstanceText">
|
||||||
|
{{ inThisInstanceText }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="result.type === 'search-global'" [attr.aria-label]="inAllText">
|
||||||
|
{{ inAllText }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
Jump to channel
|
||||||
|
<span class="d-inline-block ml-1 v-align-middle">↵</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
|
@ -0,0 +1,32 @@
|
||||||
|
@import '_mixins';
|
||||||
|
|
||||||
|
a {
|
||||||
|
@include disable-default-a-behaviour;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&, &:hover {
|
||||||
|
color: var(--mainForegroundColor);
|
||||||
|
|
||||||
|
&.focus-visible {
|
||||||
|
background-color: var(--mainHoverColor);
|
||||||
|
color: var(--mainBackgroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray {
|
||||||
|
background-color: var(--mainBackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-light {
|
||||||
|
color: var(--mainForegroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
my-global-icon {
|
||||||
|
width: 17px;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
margin: 5px;
|
||||||
|
|
||||||
|
@include apply-svg-color(var(--mainForegroundColor));
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Input, Component, Output, EventEmitter, OnInit } from '@angular/core'
|
||||||
|
import { RouterLink } from '@angular/router'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { ListKeyManagerOption } from '@angular/cdk/a11y'
|
||||||
|
|
||||||
|
type Result = {
|
||||||
|
text: string
|
||||||
|
type: 'channel' | 'suggestion' | 'search-channel' | 'search-instance' | 'search-global' | 'search-any'
|
||||||
|
routerLink?: RouterLink
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-suggestion',
|
||||||
|
templateUrl: './suggestion.component.html',
|
||||||
|
styleUrls: [ './suggestion.component.scss' ]
|
||||||
|
})
|
||||||
|
export class SuggestionComponent implements OnInit, ListKeyManagerOption {
|
||||||
|
@Input() result: Result
|
||||||
|
@Input() highlight: string
|
||||||
|
@Output() selected = new EventEmitter()
|
||||||
|
|
||||||
|
inAllText: string
|
||||||
|
inThisChannelText: string
|
||||||
|
inThisInstanceText: string
|
||||||
|
|
||||||
|
disabled = false
|
||||||
|
active = false
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private i18n: I18n
|
||||||
|
) {
|
||||||
|
this.inAllText = this.i18n('In the vidiverse')
|
||||||
|
this.inThisChannelText = this.i18n('In this channel')
|
||||||
|
this.inThisInstanceText = this.i18n('In this instance')
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabel () {
|
||||||
|
return this.result.text
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.active = false
|
||||||
|
}
|
||||||
|
|
||||||
|
selectItem () {
|
||||||
|
this.selected.emit(this.result)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren } from '@angular/core'
|
||||||
|
import { SuggestionComponent } from './suggestion.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-suggestions',
|
||||||
|
template: `
|
||||||
|
<ul role="listbox" class="p-0 m-0">
|
||||||
|
<li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
|
||||||
|
role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
|
||||||
|
<my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class SuggestionsComponent implements AfterViewInit {
|
||||||
|
@Input() results: any[]
|
||||||
|
@Input() highlight: string
|
||||||
|
@ViewChildren(SuggestionComponent) listItems: QueryList<SuggestionComponent>
|
||||||
|
@Output() init = new EventEmitter()
|
||||||
|
|
||||||
|
ngAfterViewInit () {
|
||||||
|
this.init.emit({ items: this.listItems })
|
||||||
|
this.listItems.changes.subscribe(
|
||||||
|
val => this.init.emit({ items: this.listItems })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverItem (index: number) {
|
||||||
|
this.init.emit({ items: this.listItems, index: index })
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue