diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-02-04 16:44:53 +0100 |
---|---|---|
committer | Rigel Kent <sendmemail@rigelk.eu> | 2020-02-13 16:35:24 +0100 |
commit | 9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110 (patch) | |
tree | f38e6f83a9d892a99f930c0a25b1a405e679cd4a /client/src/app | |
parent | ece3029bd99a76b3c48a1cc8c58914c5cf61f106 (diff) | |
download | PeerTube-9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110.tar.gz PeerTube-9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110.tar.zst PeerTube-9b8a7aa8ea128f7e197ff38ca9f86ffa53bbe110.zip |
Improve search typeahead performance and use native events
Diffstat (limited to 'client/src/app')
-rw-r--r-- | client/src/app/+accounts/accounts.component.html | 15 | ||||
-rw-r--r-- | client/src/app/+video-channels/video-channels.component.html | 24 | ||||
-rw-r--r-- | client/src/app/+video-channels/video-channels.component.scss | 20 | ||||
-rw-r--r-- | client/src/app/header/header.component.html | 8 | ||||
-rw-r--r-- | client/src/app/header/header.component.scss | 24 | ||||
-rw-r--r-- | client/src/app/header/header.component.ts | 16 | ||||
-rw-r--r-- | client/src/app/header/search-typeahead.component.html | 21 | ||||
-rw-r--r-- | client/src/app/header/search-typeahead.component.scss | 30 | ||||
-rw-r--r-- | client/src/app/header/search-typeahead.component.ts | 86 | ||||
-rw-r--r-- | client/src/app/header/suggestion.component.html | 12 | ||||
-rw-r--r-- | client/src/app/header/suggestion.component.ts | 18 | ||||
-rw-r--r-- | client/src/app/header/suggestions.component.html | 6 | ||||
-rw-r--r-- | client/src/app/header/suggestions.component.ts | 14 | ||||
-rw-r--r-- | client/src/app/shared/angular/highlight.pipe.ts | 74 |
14 files changed, 169 insertions, 199 deletions
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index b982fba9a..6a76393b9 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -7,14 +7,13 @@ | |||
7 | <div class="actor-info"> | 7 | <div class="actor-info"> |
8 | <div class="actor-names"> | 8 | <div class="actor-names"> |
9 | <div class="actor-display-name">{{ account.displayName }}</div> | 9 | <div class="actor-display-name">{{ account.displayName }}</div> |
10 | <div class="actor-name">{{ account.nameWithHost }} | 10 | <div class="actor-name"> |
11 | 11 | <span>{{ account.nameWithHost }}</span> | |
12 | <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" | 12 | <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" |
13 | class="btn btn-outline-secondary btn-sm copy-button" | 13 | class="btn btn-outline-secondary btn-sm copy-button" |
14 | > | 14 | > |
15 | <span class="glyphicon glyphicon-copy"></span> | 15 | <span class="glyphicon glyphicon-copy"></span> |
16 | </button> | 16 | </button> |
17 | |||
18 | </div> | 17 | </div> |
19 | <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> | 18 | <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> |
20 | <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> | 19 | <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> |
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index 735a8f2c8..1087de113 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html | |||
@@ -7,25 +7,29 @@ | |||
7 | <div class="actor-info"> | 7 | <div class="actor-info"> |
8 | <div class="actor-names"> | 8 | <div class="actor-names"> |
9 | <div class="actor-display-name">{{ videoChannel.displayName }}</div> | 9 | <div class="actor-display-name">{{ videoChannel.displayName }}</div> |
10 | <div class="actor-name">{{ videoChannel.nameWithHost }} | 10 | <div class="actor-name"> |
11 | <span>{{ videoChannel.nameWithHost }}</span> | ||
11 | <button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()" | 12 | <button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()" |
12 | class="btn btn-outline-secondary btn-sm copy-button" | 13 | class="btn btn-outline-secondary btn-sm copy-button" |
13 | > | 14 | > |
14 | <span class="glyphicon glyphicon-copy"></span> | 15 | <span class="glyphicon glyphicon-copy"></span> |
15 | </button> | 16 | </button> |
16 | </div> | 17 | </div> |
18 | </div> | ||
17 | 19 | ||
18 | <div class="right-buttons"> | 20 | <div class="right-buttons"> |
19 | <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a> | 21 | <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a> |
20 | <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> | 22 | <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> |
21 | </div> | ||
22 | </div> | 23 | </div> |
23 | <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> | ||
24 | 24 | ||
25 | <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> | 25 | <div class="actor-lower"> |
26 | <span i18n>Created by {{ videoChannel.ownerBy }}</span> | 26 | <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> |
27 | <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | 27 | |
28 | </a> | 28 | <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> |
29 | <span i18n>Created by {{ videoChannel.ownerBy }}</span> | ||
30 | <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> | ||
31 | </a> | ||
32 | </div> | ||
29 | </div> | 33 | </div> |
30 | </div> | 34 | </div> |
31 | 35 | ||
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss index 50b69e7ac..aa26a7e7b 100644 --- a/client/src/app/+video-channels/video-channels.component.scss +++ b/client/src/app/+video-channels/video-channels.component.scss | |||
@@ -8,6 +8,23 @@ | |||
8 | width: 100%; | 8 | width: 100%; |
9 | } | 9 | } |
10 | 10 | ||
11 | .actor-info { | ||
12 | display: grid !important; | ||
13 | grid-template-columns: 1fr auto; | ||
14 | grid-template-rows: 1fr auto / 1fr auto; | ||
15 | grid-template-areas: "name buttons" | ||
16 | "lower buttons"; | ||
17 | |||
18 | @media screen and (max-width: #{map-get($grid-breakpoints, lg)}) { | ||
19 | grid-template-areas: "name name" | ||
20 | "lower buttons"; | ||
21 | } | ||
22 | } | ||
23 | |||
24 | .actor-names { | ||
25 | grid-area: name; | ||
26 | } | ||
27 | |||
11 | .actor-name { | 28 | .actor-name { |
12 | flex-grow: 1; | 29 | flex-grow: 1; |
13 | 30 | ||
@@ -25,6 +42,9 @@ | |||
25 | margin-left: auto; | 42 | margin-left: auto; |
26 | margin-top: 20px; | 43 | margin-top: 20px; |
27 | 44 | ||
45 | grid-row: buttons-start / span buttons-end; | ||
46 | grid-column: buttons-start; | ||
47 | |||
28 | a { | 48 | a { |
29 | @include peertube-button-outline; | 49 | @include peertube-button-outline; |
30 | line-height: 1.8; | 50 | line-height: 1.8; |
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index 8e1d24ea8..49e219187 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html | |||
@@ -1,10 +1,4 @@ | |||
1 | <my-search-typeahead class="w-100 d-flex justify-content-end"> | 1 | <my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead> |
2 | <input | ||
3 | type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" | ||
4 | i18n-placeholder placeholder="Search videos, channels…" [(ngModel)]="searchValue" | ||
5 | > | ||
6 | <span class="icon icon-search"></span> | ||
7 | </my-search-typeahead> | ||
8 | 2 | ||
9 | <a class="upload-button" routerLink="/videos/upload"> | 3 | <a class="upload-button" routerLink="/videos/upload"> |
10 | <my-global-icon iconName="upload"></my-global-icon> | 4 | <my-global-icon iconName="upload"></my-global-icon> |
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss index 2f0a407fc..91b390773 100644 --- a/client/src/app/header/header.component.scss +++ b/client/src/app/header/header.component.scss | |||
@@ -5,30 +5,6 @@ my-search-typeahead { | |||
5 | margin-right: 15px; | 5 | margin-right: 15px; |
6 | } | 6 | } |
7 | 7 | ||
8 | #search-video { | ||
9 | @include peertube-input-text($search-input-width); | ||
10 | padding-left: 10px; | ||
11 | padding-right: 40px; // For the search icon | ||
12 | font-size: 14px; | ||
13 | |||
14 | &::placeholder { | ||
15 | color: var(--inputPlaceholderColor); | ||
16 | } | ||
17 | } | ||
18 | |||
19 | .icon.icon-search { | ||
20 | @include icon(25px); | ||
21 | height: 21px; | ||
22 | |||
23 | background-color: var(--mainForegroundColor); | ||
24 | mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%; | ||
25 | |||
26 | // yolo | ||
27 | position: absolute; | ||
28 | margin-left: -35px; | ||
29 | margin-top: 5px; | ||
30 | } | ||
31 | |||
32 | .upload-button { | 8 | .upload-button { |
33 | @include peertube-button-link; | 9 | @include peertube-button-link; |
34 | @include orange-button; | 10 | @include orange-button; |
diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts index d9311c554..cce76b0d1 100644 --- a/client/src/app/header/header.component.ts +++ b/client/src/app/header/header.component.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component } from '@angular/core' |
2 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
3 | 2 | ||
4 | @Component({ | 3 | @Component({ |
5 | selector: 'my-header', | 4 | selector: 'my-header', |
@@ -7,15 +6,4 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
7 | styleUrls: [ './header.component.scss' ] | 6 | styleUrls: [ './header.component.scss' ] |
8 | }) | 7 | }) |
9 | 8 | ||
10 | export class HeaderComponent implements OnInit { | 9 | export class HeaderComponent {} |
11 | searchValue = '' | ||
12 | ariaLabelTextForSearch = '' | ||
13 | |||
14 | constructor ( | ||
15 | private i18n: I18n | ||
16 | ) {} | ||
17 | |||
18 | ngOnInit () { | ||
19 | this.ariaLabelTextForSearch = this.i18n('Search videos, channels') | ||
20 | } | ||
21 | } | ||
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html index 2623ba337..428246585 100644 --- a/client/src/app/header/search-typeahead.component.html +++ b/client/src/app/header/search-typeahead.component.html | |||
@@ -1,9 +1,13 @@ | |||
1 | <div class="d-inline-flex position-relative" id="typeahead-container" #contentWrapper> | 1 | <div class="d-inline-flex position-relative" id="typeahead-container"> |
2 | <ng-content></ng-content> | 2 | <input |
3 | type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…" | ||
4 | [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keyup)="handleKeyUp($event)" | ||
5 | > | ||
6 | <span class="icon icon-search" (click)="doSearch()"></span> | ||
3 | 7 | ||
4 | <div class="position-absolute jump-to-suggestions"> | 8 | <div class="position-absolute jump-to-suggestions"> |
5 | <!-- suggestions --> | 9 | <!-- suggestions --> |
6 | <my-suggestions *ngIf="hasSearch && newSearch" [results]="results" [highlight]="searchInput.value" (init)="initKeyboardEventsManager($event)" #suggestions></my-suggestions> | 10 | <my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions> |
7 | 11 | ||
8 | <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion --> | 12 | <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion --> |
9 | <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden"> | 13 | <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden"> |
@@ -11,7 +15,7 @@ | |||
11 | <div class="d-flex justify-content-between"> | 15 | <div class="d-flex justify-content-between"> |
12 | <label class="small-title" i18n>Global search</label> | 16 | <label class="small-title" i18n>Global search</label> |
13 | <div class="advanced-search-status text-muted"> | 17 | <div class="advanced-search-status text-muted"> |
14 | <span class="mr-1" i18n>using {{ globalSearchIndex }}</span> | 18 | <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span> |
15 | <i class="glyphicon glyphicon-globe"></i> | 19 | <i class="glyphicon glyphicon-globe"></i> |
16 | </div> | 20 | </div> |
17 | </div> | 21 | </div> |
@@ -20,13 +24,14 @@ | |||
20 | </div> | 24 | </div> |
21 | 25 | ||
22 | <!-- search instructions, when search input is empty --> | 26 | <!-- search instructions, when search input is empty --> |
23 | <div *ngIf="!hasSearch" id="typeahead-instructions" class="overflow-hidden"> | 27 | <div *ngIf="showInstructions" id="typeahead-instructions" class="overflow-hidden"> |
24 | <div class="d-flex justify-content-between"> | 28 | <div class="d-flex justify-content-between"> |
25 | <label class="small-title" i18n>Advanced search</label> | 29 | <label class="small-title" i18n>Advanced search</label> |
26 | <div class="advanced-search-status c-help"> | 30 | <div class="advanced-search-status c-help"> |
27 | <span [ngClass]="URIPolicy === 'any' ? 'text-success' : 'text-muted'" [title]="URIPolicyText"> | 31 | <span [ngClass]="anyURI ? '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."> |
28 | <span class="mr-1" i18n>{URIPolicy, select, only-followed {only followed instances} other {any instance}} </span> | 32 | <span *ngIf="anyURI" class="mr-1" i18n>any instance</span> |
29 | <i [ngClass]="URIPolicy === 'any' ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i> | 33 | <span *ngIf="!anyURI" class="mr-1" i18n>only followed instances</span> |
34 | <i [ngClass]="anyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i> | ||
30 | </span> | 35 | </span> |
31 | </div> | 36 | </div> |
32 | </div> | 37 | </div> |
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss index 6d7511c70..a55e78326 100644 --- a/client/src/app/header/search-typeahead.component.scss +++ b/client/src/app/header/search-typeahead.component.scss | |||
@@ -3,6 +3,30 @@ | |||
3 | @import '_bootstrap-variables'; | 3 | @import '_bootstrap-variables'; |
4 | @import '~bootstrap/scss/mixins/_breakpoints'; | 4 | @import '~bootstrap/scss/mixins/_breakpoints'; |
5 | 5 | ||
6 | #search-video { | ||
7 | @include peertube-input-text($search-input-width); | ||
8 | padding-left: 10px; | ||
9 | padding-right: 40px; // For the search icon | ||
10 | font-size: 14px; | ||
11 | |||
12 | &::placeholder { | ||
13 | color: var(--inputPlaceholderColor); | ||
14 | } | ||
15 | } | ||
16 | |||
17 | .icon.icon-search { | ||
18 | @include icon(25px); | ||
19 | height: 21px; | ||
20 | |||
21 | background-color: var(--mainForegroundColor); | ||
22 | mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%; | ||
23 | |||
24 | // yolo | ||
25 | position: absolute; | ||
26 | margin-left: -35px; | ||
27 | margin-top: 5px; | ||
28 | } | ||
29 | |||
6 | .jump-to-suggestions { | 30 | .jump-to-suggestions { |
7 | top: 100%; | 31 | top: 100%; |
8 | left: 0; | 32 | left: 0; |
@@ -42,7 +66,7 @@ my-suggestions ::ng-deep ul { | |||
42 | } | 66 | } |
43 | 67 | ||
44 | #typeahead-container { | 68 | #typeahead-container { |
45 | ::ng-deep input { | 69 | input { |
46 | border: 1px solid var(--mainBackgroundColor) !important; | 70 | border: 1px solid var(--mainBackgroundColor) !important; |
47 | box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px; | 71 | box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px; |
48 | flex-grow: 1; | 72 | flex-grow: 1; |
@@ -56,12 +80,12 @@ my-suggestions ::ng-deep ul { | |||
56 | @media screen and (max-width: $small-view) { | 80 | @media screen and (max-width: $small-view) { |
57 | flex: 1; | 81 | flex: 1; |
58 | 82 | ||
59 | ::ng-deep input { | 83 | input { |
60 | width: unset; | 84 | width: unset; |
61 | } | 85 | } |
62 | } | 86 | } |
63 | 87 | ||
64 | ::ng-deep span { | 88 | span { |
65 | right: 10px; | 89 | right: 10px; |
66 | } | 90 | } |
67 | 91 | ||
diff --git a/client/src/app/header/search-typeahead.component.ts b/client/src/app/header/search-typeahead.component.ts index 9b414bc56..c265f2c83 100644 --- a/client/src/app/header/search-typeahead.component.ts +++ b/client/src/app/header/search-typeahead.component.ts | |||
@@ -1,16 +1,15 @@ | |||
1 | import { | 1 | import { |
2 | Component, | 2 | Component, |
3 | ViewChild, | ||
4 | ElementRef, | ||
5 | AfterViewInit, | 3 | AfterViewInit, |
6 | OnInit, | 4 | OnInit, |
7 | OnDestroy, | 5 | OnDestroy, |
8 | QueryList | 6 | QueryList, |
7 | ViewChild, | ||
8 | ElementRef | ||
9 | } from '@angular/core' | 9 | } from '@angular/core' |
10 | import { Router, NavigationEnd, Params, ActivatedRoute } from '@angular/router' | 10 | import { Router, Params, ActivatedRoute } from '@angular/router' |
11 | import { AuthService, ServerService } from '@app/core' | 11 | import { AuthService, ServerService } from '@app/core' |
12 | import { I18n } from '@ngx-translate/i18n-polyfill' | 12 | import { first, tap } from 'rxjs/operators' |
13 | import { filter, first, tap, map } from 'rxjs/operators' | ||
14 | import { ListKeyManager } from '@angular/cdk/a11y' | 13 | import { ListKeyManager } from '@angular/cdk/a11y' |
15 | import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes' | 14 | import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes' |
16 | import { SuggestionComponent, Result } from './suggestion.component' | 15 | import { SuggestionComponent, Result } from './suggestion.component' |
@@ -24,19 +23,16 @@ import { ServerConfig } from '@shared/models' | |||
24 | styleUrls: [ './search-typeahead.component.scss' ] | 23 | styleUrls: [ './search-typeahead.component.scss' ] |
25 | }) | 24 | }) |
26 | export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit { | 25 | export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit { |
27 | @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef | 26 | @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement> |
28 | 27 | ||
29 | hasChannel = false | 28 | hasChannel = false |
30 | inChannel = false | 29 | inChannel = false |
31 | newSearch = true | 30 | newSearch = true |
32 | 31 | ||
33 | searchInput: HTMLInputElement | 32 | search = '' |
34 | serverConfig: ServerConfig | 33 | serverConfig: ServerConfig |
35 | 34 | ||
36 | URIPolicyText: string | ||
37 | inAllText: string | ||
38 | inThisChannelText: string | 35 | inThisChannelText: string |
39 | globalSearchIndex = 'https://index.joinpeertube.org' | ||
40 | 36 | ||
41 | keyboardEventsManager: ListKeyManager<SuggestionComponent> | 37 | keyboardEventsManager: ListKeyManager<SuggestionComponent> |
42 | results: any[] = [] | 38 | results: any[] = [] |
@@ -45,30 +41,10 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni | |||
45 | private authService: AuthService, | 41 | private authService: AuthService, |
46 | private router: Router, | 42 | private router: Router, |
47 | private route: ActivatedRoute, | 43 | private route: ActivatedRoute, |
48 | private serverService: ServerService, | 44 | private serverService: ServerService |
49 | private i18n: I18n | 45 | ) {} |
50 | ) { | ||
51 | this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, or if your instance only allows doing so for instances it follows.') | ||
52 | this.inAllText = this.i18n('In all PeerTube') | ||
53 | this.inThisChannelText = this.i18n('In this channel') | ||
54 | } | ||
55 | 46 | ||
56 | ngOnInit () { | 47 | ngOnInit () { |
57 | this.router.events | ||
58 | .pipe(filter(e => e instanceof NavigationEnd)) | ||
59 | .subscribe((event: NavigationEnd) => { | ||
60 | this.hasChannel = event.url.startsWith('/videos/watch') | ||
61 | this.inChannel = event.url.startsWith('/video-channels') | ||
62 | this.computeResults() | ||
63 | }) | ||
64 | |||
65 | this.router.events | ||
66 | .pipe( | ||
67 | filter(e => e instanceof NavigationEnd), | ||
68 | map(() => getParameterByName('search', window.location.href)) | ||
69 | ) | ||
70 | .subscribe(searchQuery => this.searchInput.value = searchQuery || '') | ||
71 | |||
72 | this.serverService.getConfig() | 48 | this.serverService.getConfig() |
73 | .subscribe(config => this.serverConfig = config) | 49 | .subscribe(config => this.serverConfig = config) |
74 | } | 50 | } |
@@ -78,53 +54,52 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni | |||
78 | } | 54 | } |
79 | 55 | ||
80 | ngAfterViewInit () { | 56 | ngAfterViewInit () { |
81 | this.searchInput = this.contentWrapper.nativeElement.childNodes[0] | 57 | this.search = getParameterByName('search', window.location.href) || '' |
82 | this.searchInput.addEventListener('input', this.computeResults.bind(this)) | ||
83 | this.searchInput.addEventListener('keyup', this.handleKeyUp.bind(this)) | ||
84 | } | ||
85 | |||
86 | get hasSearch () { | ||
87 | return !!this.searchInput && !!this.searchInput.value | ||
88 | } | 58 | } |
89 | 59 | ||
90 | get activeResult () { | 60 | get activeResult () { |
91 | return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result | 61 | return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result |
92 | } | 62 | } |
93 | 63 | ||
64 | get showInstructions () { | ||
65 | return !this.search | ||
66 | } | ||
67 | |||
94 | get showHelp () { | 68 | get showHelp () { |
95 | return this.hasSearch && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false | 69 | return this.search && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false |
96 | } | 70 | } |
97 | 71 | ||
98 | get URIPolicy (): 'only-followed' | 'any' { | 72 | get anyURI () { |
99 | return ( | 73 | if (!this.serverConfig) return false |
100 | this.authService.isLoggedIn() | 74 | return this.authService.isLoggedIn() |
101 | ? this.serverConfig.search.remoteUri.users | 75 | ? this.serverConfig.search.remoteUri.users |
102 | : this.serverConfig.search.remoteUri.anonymous | 76 | : this.serverConfig.search.remoteUri.anonymous |
103 | ) | 77 | } |
104 | ? 'any' | 78 | |
105 | : 'only-followed' | 79 | onSearchChange () { |
80 | this.computeResults() | ||
106 | } | 81 | } |
107 | 82 | ||
108 | computeResults () { | 83 | computeResults () { |
109 | this.newSearch = true | 84 | this.newSearch = true |
110 | let results: Result[] = [] | 85 | let results: Result[] = [] |
111 | 86 | ||
112 | if (this.hasSearch) { | 87 | if (this.search) { |
113 | results = [ | 88 | results = [ |
114 | /* Channel search is still unimplemented. Uncomment when it is. | 89 | /* Channel search is still unimplemented. Uncomment when it is. |
115 | { | 90 | { |
116 | text: this.searchInput.value, | 91 | text: this.search, |
117 | type: 'search-channel' | 92 | type: 'search-channel' |
118 | }, | 93 | }, |
119 | */ | 94 | */ |
120 | { | 95 | { |
121 | text: this.searchInput.value, | 96 | text: this.search, |
122 | type: 'search-instance', | 97 | type: 'search-instance', |
123 | default: true | 98 | default: true |
124 | }, | 99 | }, |
125 | /* Global search is still unimplemented. Uncomment when it is. | 100 | /* Global search is still unimplemented. Uncomment when it is. |
126 | { | 101 | { |
127 | text: this.searchInput.value, | 102 | text: this.search, |
128 | type: 'search-global' | 103 | type: 'search-global' |
129 | }, | 104 | }, |
130 | */ | 105 | */ |
@@ -137,7 +112,8 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni | |||
137 | // if we're not in a channel or one of its videos/playlits, show all channel-related results | 112 | // if we're not in a channel or one of its videos/playlits, show all channel-related results |
138 | if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') | 113 | if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel') |
139 | // if we're in a channel, show all channel-related results except for the channel redirection itself | 114 | // if we're in a channel, show all channel-related results except for the channel redirection itself |
140 | if (this.inChannel) return !(result.type === 'channel') | 115 | if (this.inChannel) return result.type !== 'channel' |
116 | // all other result types are kept | ||
141 | return true | 117 | return true |
142 | } | 118 | } |
143 | ) | 119 | ) |
@@ -187,7 +163,7 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni | |||
187 | Object.assign(queryParams, this.route.snapshot.queryParams) | 163 | Object.assign(queryParams, this.route.snapshot.queryParams) |
188 | } | 164 | } |
189 | 165 | ||
190 | Object.assign(queryParams, { search: this.searchInput.value }) | 166 | Object.assign(queryParams, { search: this.search }) |
191 | 167 | ||
192 | const o = this.authService.isLoggedIn() | 168 | const o = this.authService.isLoggedIn() |
193 | ? this.loadUserLanguagesIfNeeded(queryParams) | 169 | ? this.loadUserLanguagesIfNeeded(queryParams) |
diff --git a/client/src/app/header/suggestion.component.html b/client/src/app/header/suggestion.component.html index 894cacb95..edde2023a 100644 --- a/client/src/app/header/suggestion.component.html +++ b/client/src/app/header/suggestion.component.html | |||
@@ -9,15 +9,9 @@ | |||
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> | 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> |
10 | 10 | ||
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"> | 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"> |
12 | <span *ngIf="result.type === 'search-channel'" [attr.aria-label]="inThisChannelText"> | 12 | <span *ngIf="result.type === 'search-channel'" i18n>In this channel</span> |
13 | {{ inThisChannelText }} | 13 | <span *ngIf="result.type === 'search-instance'" i18n>In this instance</span> |
14 | </span> | 14 | <span *ngIf="result.type === 'search-global'" i18n>In the vidiverse</span> |
15 | <span *ngIf="result.type === 'search-instance'" [attr.aria-label]="inThisInstanceText"> | ||
16 | {{ inThisInstanceText }} | ||
17 | </span> | ||
18 | <span *ngIf="result.type === 'search-global'" [attr.aria-label]="inAllText"> | ||
19 | {{ inAllText }} | ||
20 | </span> | ||
21 | <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span> | 15 | <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span> |
22 | </div> | 16 | </div> |
23 | 17 | ||
diff --git a/client/src/app/header/suggestion.component.ts b/client/src/app/header/suggestion.component.ts index bdcb3e03f..69641b511 100644 --- a/client/src/app/header/suggestion.component.ts +++ b/client/src/app/header/suggestion.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Input, Component, Output, EventEmitter, OnInit } from '@angular/core' | 1 | import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core' |
2 | import { RouterLink } from '@angular/router' | 2 | import { RouterLink } from '@angular/router' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { ListKeyManagerOption } from '@angular/cdk/a11y' | 3 | import { ListKeyManagerOption } from '@angular/cdk/a11y' |
5 | 4 | ||
6 | export type Result = { | 5 | export type Result = { |
@@ -13,28 +12,17 @@ export type Result = { | |||
13 | @Component({ | 12 | @Component({ |
14 | selector: 'my-suggestion', | 13 | selector: 'my-suggestion', |
15 | templateUrl: './suggestion.component.html', | 14 | templateUrl: './suggestion.component.html', |
16 | styleUrls: [ './suggestion.component.scss' ] | 15 | styleUrls: [ './suggestion.component.scss' ], |
16 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | 17 | }) |
18 | export class SuggestionComponent implements OnInit, ListKeyManagerOption { | 18 | export class SuggestionComponent implements OnInit, ListKeyManagerOption { |
19 | @Input() result: Result | 19 | @Input() result: Result |
20 | @Input() highlight: string | 20 | @Input() highlight: string |
21 | @Output() selected = new EventEmitter() | 21 | @Output() selected = new EventEmitter() |
22 | 22 | ||
23 | inAllText: string | ||
24 | inThisChannelText: string | ||
25 | inThisInstanceText: string | ||
26 | |||
27 | disabled = false | 23 | disabled = false |
28 | active = false | 24 | active = false |
29 | 25 | ||
30 | constructor ( | ||
31 | private i18n: I18n | ||
32 | ) { | ||
33 | this.inAllText = this.i18n('In the vidiverse') | ||
34 | this.inThisChannelText = this.i18n('In this channel') | ||
35 | this.inThisInstanceText = this.i18n('In this instance') | ||
36 | } | ||
37 | |||
38 | getLabel () { | 26 | getLabel () { |
39 | return this.result.text | 27 | return this.result.text |
40 | } | 28 | } |
diff --git a/client/src/app/header/suggestions.component.html b/client/src/app/header/suggestions.component.html new file mode 100644 index 000000000..8d017d78d --- /dev/null +++ b/client/src/app/header/suggestions.component.html | |||
@@ -0,0 +1,6 @@ | |||
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 index fac7fe2f9..ee3ef73c2 100644 --- a/client/src/app/header/suggestions.component.ts +++ b/client/src/app/header/suggestions.component.ts | |||
@@ -1,16 +1,10 @@ | |||
1 | import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren } from '@angular/core' | 1 | import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core' |
2 | import { SuggestionComponent } from './suggestion.component' | 2 | import { SuggestionComponent } from './suggestion.component' |
3 | 3 | ||
4 | @Component({ | 4 | @Component({ |
5 | selector: 'my-suggestions', | 5 | selector: 'my-suggestions', |
6 | template: ` | 6 | templateUrl: './suggestions.component.html', |
7 | <ul role="listbox" class="p-0 m-0"> | 7 | changeDetection: ChangeDetectionStrategy.OnPush |
8 | <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5" | ||
9 | role="option" aria-selected="true" (mouseenter)="hoverItem(i)"> | ||
10 | <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion> | ||
11 | </li> | ||
12 | </ul> | ||
13 | ` | ||
14 | }) | 8 | }) |
15 | export class SuggestionsComponent implements AfterViewInit { | 9 | export class SuggestionsComponent implements AfterViewInit { |
16 | @Input() results: any[] | 10 | @Input() results: any[] |
@@ -20,7 +14,7 @@ export class SuggestionsComponent implements AfterViewInit { | |||
20 | 14 | ||
21 | ngAfterViewInit () { | 15 | ngAfterViewInit () { |
22 | this.listItems.changes.subscribe( | 16 | this.listItems.changes.subscribe( |
23 | val => this.init.emit({ items: this.listItems }) | 17 | _ => this.init.emit({ items: this.listItems }) |
24 | ) | 18 | ) |
25 | } | 19 | } |
26 | 20 | ||
diff --git a/client/src/app/shared/angular/highlight.pipe.ts b/client/src/app/shared/angular/highlight.pipe.ts index 4199d833e..e219b3823 100644 --- a/client/src/app/shared/angular/highlight.pipe.ts +++ b/client/src/app/shared/angular/highlight.pipe.ts | |||
@@ -5,48 +5,50 @@ import { SafeHtml } from '@angular/platform-browser' | |||
5 | @Pipe({ name: 'highlight' }) | 5 | @Pipe({ name: 'highlight' }) |
6 | export class HighlightPipe implements PipeTransform { | 6 | export class HighlightPipe implements PipeTransform { |
7 | /* use this for single match search */ | 7 | /* use this for single match search */ |
8 | static SINGLE_MATCH: string = "Single-Match" | 8 | static SINGLE_MATCH = 'Single-Match' |
9 | /* use this for single match search with a restriction that target should start with search string */ | 9 | /* use this for single match search with a restriction that target should start with search string */ |
10 | static SINGLE_AND_STARTS_WITH_MATCH: string = "Single-And-StartsWith-Match" | 10 | static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match' |
11 | /* use this for global search */ | 11 | /* use this for global search */ |
12 | static MULTI_MATCH: string = "Multi-Match" | 12 | static MULTI_MATCH = 'Multi-Match' |
13 | 13 | ||
14 | constructor() {} | 14 | // tslint:disable-next-line:no-empty |
15 | transform( | 15 | constructor () {} |
16 | |||
17 | transform ( | ||
16 | contentString: string = null, | 18 | contentString: string = null, |
17 | stringToHighlight: string = null, | 19 | stringToHighlight: string = null, |
18 | option: string = "Single-And-StartsWith-Match", | 20 | option = 'Single-And-StartsWith-Match', |
19 | caseSensitive: boolean = false, | 21 | caseSensitive = false, |
20 | highlightStyleName: string = "search-highlight" | 22 | highlightStyleName = 'search-highlight' |
21 | ): SafeHtml { | 23 | ): SafeHtml { |
22 | if (stringToHighlight && contentString && option) { | 24 | if (stringToHighlight && contentString && option) { |
23 | let regex: any = "" | 25 | let regex: any = '' |
24 | let caseFlag: string = !caseSensitive ? "i" : "" | 26 | const caseFlag: string = !caseSensitive ? 'i' : '' |
25 | switch (option) { | 27 | switch (option) { |
26 | case "Single-Match": { | 28 | case 'Single-Match': { |
27 | regex = new RegExp(stringToHighlight, caseFlag) | 29 | regex = new RegExp(stringToHighlight, caseFlag) |
28 | break | 30 | break |
29 | } | 31 | } |
30 | case "Single-And-StartsWith-Match": { | 32 | case 'Single-And-StartsWith-Match': { |
31 | regex = new RegExp("^" + stringToHighlight, caseFlag) | 33 | regex = new RegExp("^" + stringToHighlight, caseFlag) |
32 | break | 34 | break |
33 | } | 35 | } |
34 | case "Multi-Match": { | 36 | case 'Multi-Match': { |
35 | regex = new RegExp(stringToHighlight, "g" + caseFlag) | 37 | regex = new RegExp(stringToHighlight, 'g' + caseFlag) |
36 | break | 38 | break |
37 | } | 39 | } |
38 | default: { | 40 | default: { |
39 | // default will be a global case-insensitive match | 41 | // default will be a global case-insensitive match |
40 | regex = new RegExp(stringToHighlight, "gi") | 42 | regex = new RegExp(stringToHighlight, 'gi') |
41 | } | 43 | } |
42 | } | ||
43 | const replaced = contentString.replace( | ||
44 | regex, | ||
45 | (match) => `<span class="${highlightStyleName}">${match}</span>` | ||
46 | ) | ||
47 | return replaced | ||
48 | } else { | ||
49 | return contentString | ||
50 | } | 44 | } |
45 | const replaced = contentString.replace( | ||
46 | regex, | ||
47 | (match) => `<span class="${highlightStyleName}">${match}</span>` | ||
48 | ) | ||
49 | return replaced | ||
50 | } else { | ||
51 | return contentString | ||
52 | } | ||
51 | } | 53 | } |
52 | } | 54 | } |