mirror of https://github.com/Chocobozzz/PeerTube
Improve search typeahead performance and use native events
parent
ece3029bd9
commit
9b8a7aa8ea
|
@ -7,14 +7,13 @@
|
|||
<div class="actor-info">
|
||||
<div class="actor-names">
|
||||
<div class="actor-display-name">{{ account.displayName }}</div>
|
||||
<div class="actor-name">{{ account.nameWithHost }}
|
||||
|
||||
<button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
|
||||
class="btn btn-outline-secondary btn-sm copy-button"
|
||||
>
|
||||
<span class="glyphicon glyphicon-copy"></span>
|
||||
</button>
|
||||
|
||||
<div class="actor-name">
|
||||
<span>{{ account.nameWithHost }}</span>
|
||||
<button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
|
||||
class="btn btn-outline-secondary btn-sm copy-button"
|
||||
>
|
||||
<span class="glyphicon glyphicon-copy"></span>
|
||||
</button>
|
||||
</div>
|
||||
<span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
|
||||
<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
|
||||
|
|
|
@ -7,25 +7,29 @@
|
|||
<div class="actor-info">
|
||||
<div class="actor-names">
|
||||
<div class="actor-display-name">{{ videoChannel.displayName }}</div>
|
||||
<div class="actor-name">{{ videoChannel.nameWithHost }}
|
||||
<div class="actor-name">
|
||||
<span>{{ videoChannel.nameWithHost }}</span>
|
||||
<button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()"
|
||||
class="btn btn-outline-secondary btn-sm copy-button"
|
||||
>
|
||||
<span class="glyphicon glyphicon-copy"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="right-buttons">
|
||||
<a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a>
|
||||
<my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
|
||||
|
||||
<a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
|
||||
<span i18n>Created by {{ videoChannel.ownerBy }}</span>
|
||||
<img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
|
||||
</a>
|
||||
<div class="right-buttons">
|
||||
<a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a>
|
||||
<my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
|
||||
</div>
|
||||
|
||||
<div class="actor-lower">
|
||||
<div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
|
||||
|
||||
<a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
|
||||
<span i18n>Created by {{ videoChannel.ownerBy }}</span>
|
||||
<img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -8,6 +8,23 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.actor-info {
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: 1fr auto / 1fr auto;
|
||||
grid-template-areas: "name buttons"
|
||||
"lower buttons";
|
||||
|
||||
@media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
|
||||
grid-template-areas: "name name"
|
||||
"lower buttons";
|
||||
}
|
||||
}
|
||||
|
||||
.actor-names {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
.actor-name {
|
||||
flex-grow: 1;
|
||||
|
||||
|
@ -25,6 +42,9 @@
|
|||
margin-left: auto;
|
||||
margin-top: 20px;
|
||||
|
||||
grid-row: buttons-start / span buttons-end;
|
||||
grid-column: buttons-start;
|
||||
|
||||
a {
|
||||
@include peertube-button-outline;
|
||||
line-height: 1.8;
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
<my-search-typeahead class="w-100 d-flex justify-content-end">
|
||||
<input
|
||||
type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch"
|
||||
i18n-placeholder placeholder="Search videos, channels…" [(ngModel)]="searchValue"
|
||||
>
|
||||
<span class="icon icon-search"></span>
|
||||
</my-search-typeahead>
|
||||
<my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead>
|
||||
|
||||
<a class="upload-button" routerLink="/videos/upload">
|
||||
<my-global-icon iconName="upload"></my-global-icon>
|
||||
|
|
|
@ -5,30 +5,6 @@ my-search-typeahead {
|
|||
margin-right: 15px;
|
||||
}
|
||||
|
||||
#search-video {
|
||||
@include peertube-input-text($search-input-width);
|
||||
padding-left: 10px;
|
||||
padding-right: 40px; // For the search icon
|
||||
font-size: 14px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--inputPlaceholderColor);
|
||||
}
|
||||
}
|
||||
|
||||
.icon.icon-search {
|
||||
@include icon(25px);
|
||||
height: 21px;
|
||||
|
||||
background-color: var(--mainForegroundColor);
|
||||
mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
|
||||
|
||||
// yolo
|
||||
position: absolute;
|
||||
margin-left: -35px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
@include peertube-button-link;
|
||||
@include orange-button;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-header',
|
||||
|
@ -7,15 +6,4 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
|
|||
styleUrls: [ './header.component.scss' ]
|
||||
})
|
||||
|
||||
export class HeaderComponent implements OnInit {
|
||||
searchValue = ''
|
||||
ariaLabelTextForSearch = ''
|
||||
|
||||
constructor (
|
||||
private i18n: I18n
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.ariaLabelTextForSearch = this.i18n('Search videos, channels')
|
||||
}
|
||||
}
|
||||
export class HeaderComponent {}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
<div class="d-inline-flex position-relative" id="typeahead-container" #contentWrapper>
|
||||
<ng-content></ng-content>
|
||||
<div class="d-inline-flex position-relative" id="typeahead-container">
|
||||
<input
|
||||
type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…"
|
||||
[(ngModel)]="search" (ngModelChange)="onSearchChange()" (keyup)="handleKeyUp($event)"
|
||||
>
|
||||
<span class="icon icon-search" (click)="doSearch()"></span>
|
||||
|
||||
<div class="position-absolute jump-to-suggestions">
|
||||
<!-- suggestions -->
|
||||
<my-suggestions *ngIf="hasSearch && newSearch" [results]="results" [highlight]="searchInput.value" (init)="initKeyboardEventsManager($event)" #suggestions></my-suggestions>
|
||||
<my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions>
|
||||
|
||||
<!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
|
||||
<div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden">
|
||||
|
@ -11,7 +15,7 @@
|
|||
<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>
|
||||
<span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span>
|
||||
<i class="glyphicon glyphicon-globe"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,13 +24,14 @@
|
|||
</div>
|
||||
|
||||
<!-- search instructions, when search input is empty -->
|
||||
<div *ngIf="!hasSearch" id="typeahead-instructions" class="overflow-hidden">
|
||||
<div *ngIf="showInstructions" id="typeahead-instructions" class="overflow-hidden">
|
||||
<div class="d-flex justify-content-between">
|
||||
<label class="small-title" i18n>Advanced search</label>
|
||||
<div class="advanced-search-status c-help">
|
||||
<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>
|
||||
<i [ngClass]="URIPolicy === 'any' ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
|
||||
<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.">
|
||||
<span *ngIf="anyURI" class="mr-1" i18n>any instance</span>
|
||||
<span *ngIf="!anyURI" class="mr-1" i18n>only followed instances</span>
|
||||
<i [ngClass]="anyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,30 @@
|
|||
@import '_bootstrap-variables';
|
||||
@import '~bootstrap/scss/mixins/_breakpoints';
|
||||
|
||||
#search-video {
|
||||
@include peertube-input-text($search-input-width);
|
||||
padding-left: 10px;
|
||||
padding-right: 40px; // For the search icon
|
||||
font-size: 14px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--inputPlaceholderColor);
|
||||
}
|
||||
}
|
||||
|
||||
.icon.icon-search {
|
||||
@include icon(25px);
|
||||
height: 21px;
|
||||
|
||||
background-color: var(--mainForegroundColor);
|
||||
mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
|
||||
|
||||
// yolo
|
||||
position: absolute;
|
||||
margin-left: -35px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.jump-to-suggestions {
|
||||
top: 100%;
|
||||
left: 0;
|
||||
|
@ -42,7 +66,7 @@ my-suggestions ::ng-deep ul {
|
|||
}
|
||||
|
||||
#typeahead-container {
|
||||
::ng-deep input {
|
||||
input {
|
||||
border: 1px solid var(--mainBackgroundColor) !important;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
|
||||
flex-grow: 1;
|
||||
|
@ -56,12 +80,12 @@ my-suggestions ::ng-deep ul {
|
|||
@media screen and (max-width: $small-view) {
|
||||
flex: 1;
|
||||
|
||||
::ng-deep input {
|
||||
input {
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep span {
|
||||
span {
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import {
|
||||
Component,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
AfterViewInit,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
QueryList
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ElementRef
|
||||
} from '@angular/core'
|
||||
import { Router, NavigationEnd, Params, ActivatedRoute } from '@angular/router'
|
||||
import { Router, Params, ActivatedRoute } from '@angular/router'
|
||||
import { AuthService, ServerService } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { filter, first, tap, map } from 'rxjs/operators'
|
||||
import { first, tap } from 'rxjs/operators'
|
||||
import { ListKeyManager } from '@angular/cdk/a11y'
|
||||
import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'
|
||||
import { SuggestionComponent, Result } from './suggestion.component'
|
||||
|
@ -24,19 +23,16 @@ import { ServerConfig } from '@shared/models'
|
|||
styleUrls: [ './search-typeahead.component.scss' ]
|
||||
})
|
||||
export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
@ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef
|
||||
@ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement>
|
||||
|
||||
hasChannel = false
|
||||
inChannel = false
|
||||
newSearch = true
|
||||
|
||||
searchInput: HTMLInputElement
|
||||
search = ''
|
||||
serverConfig: ServerConfig
|
||||
|
||||
URIPolicyText: string
|
||||
inAllText: string
|
||||
inThisChannelText: string
|
||||
globalSearchIndex = 'https://index.joinpeertube.org'
|
||||
|
||||
keyboardEventsManager: ListKeyManager<SuggestionComponent>
|
||||
results: any[] = []
|
||||
|
@ -45,30 +41,10 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
|
|||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private serverService: ServerService,
|
||||
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.inAllText = this.i18n('In all PeerTube')
|
||||
this.inThisChannelText = this.i18n('In this channel')
|
||||
}
|
||||
private serverService: ServerService
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.router.events
|
||||
.pipe(filter(e => e instanceof NavigationEnd))
|
||||
.subscribe((event: NavigationEnd) => {
|
||||
this.hasChannel = event.url.startsWith('/videos/watch')
|
||||
this.inChannel = event.url.startsWith('/video-channels')
|
||||
this.computeResults()
|
||||
})
|
||||
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter(e => e instanceof NavigationEnd),
|
||||
map(() => getParameterByName('search', window.location.href))
|
||||
)
|
||||
.subscribe(searchQuery => this.searchInput.value = searchQuery || '')
|
||||
|
||||
this.serverService.getConfig()
|
||||
.subscribe(config => this.serverConfig = config)
|
||||
}
|
||||
|
@ -78,53 +54,52 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
|
|||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.searchInput = this.contentWrapper.nativeElement.childNodes[0]
|
||||
this.searchInput.addEventListener('input', this.computeResults.bind(this))
|
||||
this.searchInput.addEventListener('keyup', this.handleKeyUp.bind(this))
|
||||
}
|
||||
|
||||
get hasSearch () {
|
||||
return !!this.searchInput && !!this.searchInput.value
|
||||
this.search = getParameterByName('search', window.location.href) || ''
|
||||
}
|
||||
|
||||
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
|
||||
get showInstructions () {
|
||||
return !this.search
|
||||
}
|
||||
|
||||
get URIPolicy (): 'only-followed' | 'any' {
|
||||
return (
|
||||
this.authService.isLoggedIn()
|
||||
? this.serverConfig.search.remoteUri.users
|
||||
: this.serverConfig.search.remoteUri.anonymous
|
||||
)
|
||||
? 'any'
|
||||
: 'only-followed'
|
||||
get showHelp () {
|
||||
return this.search && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false
|
||||
}
|
||||
|
||||
get anyURI () {
|
||||
if (!this.serverConfig) return false
|
||||
return this.authService.isLoggedIn()
|
||||
? this.serverConfig.search.remoteUri.users
|
||||
: this.serverConfig.search.remoteUri.anonymous
|
||||
}
|
||||
|
||||
onSearchChange () {
|
||||
this.computeResults()
|
||||
}
|
||||
|
||||
computeResults () {
|
||||
this.newSearch = true
|
||||
let results: Result[] = []
|
||||
|
||||
if (this.hasSearch) {
|
||||
if (this.search) {
|
||||
results = [
|
||||
/* Channel search is still unimplemented. Uncomment when it is.
|
||||
{
|
||||
text: this.searchInput.value,
|
||||
text: this.search,
|
||||
type: 'search-channel'
|
||||
},
|
||||
*/
|
||||
{
|
||||
text: this.searchInput.value,
|
||||
text: this.search,
|
||||
type: 'search-instance',
|
||||
default: true
|
||||
},
|
||||
/* Global search is still unimplemented. Uncomment when it is.
|
||||
{
|
||||
text: this.searchInput.value,
|
||||
text: this.search,
|
||||
type: 'search-global'
|
||||
},
|
||||
*/
|
||||
|
@ -137,7 +112,8 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
|
|||
// 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 we're in a channel, show all channel-related results except for the channel redirection itself
|
||||
if (this.inChannel) return !(result.type === 'channel')
|
||||
if (this.inChannel) return result.type !== 'channel'
|
||||
// all other result types are kept
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
@ -187,7 +163,7 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
|
|||
Object.assign(queryParams, this.route.snapshot.queryParams)
|
||||
}
|
||||
|
||||
Object.assign(queryParams, { search: this.searchInput.value })
|
||||
Object.assign(queryParams, { search: this.search })
|
||||
|
||||
const o = this.authService.isLoggedIn()
|
||||
? this.loadUserLanguagesIfNeeded(queryParams)
|
||||
|
|
|
@ -9,15 +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>
|
||||
|
||||
<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-channel'" i18n>In this channel</span>
|
||||
<span *ngIf="result.type === 'search-instance'" i18n>In this instance</span>
|
||||
<span *ngIf="result.type === 'search-global'" i18n>In the vidiverse</span>
|
||||
<span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Input, Component, Output, EventEmitter, OnInit } from '@angular/core'
|
||||
import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { ListKeyManagerOption } from '@angular/cdk/a11y'
|
||||
|
||||
export type Result = {
|
||||
|
@ -13,28 +12,17 @@ export type Result = {
|
|||
@Component({
|
||||
selector: 'my-suggestion',
|
||||
templateUrl: './suggestion.component.html',
|
||||
styleUrls: [ './suggestion.component.scss' ]
|
||||
styleUrls: [ './suggestion.component.scss' ],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<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>
|
|
@ -1,16 +1,10 @@
|
|||
import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren } from '@angular/core'
|
||||
import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } 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>
|
||||
`
|
||||
templateUrl: './suggestions.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SuggestionsComponent implements AfterViewInit {
|
||||
@Input() results: any[]
|
||||
|
@ -20,7 +14,7 @@ export class SuggestionsComponent implements AfterViewInit {
|
|||
|
||||
ngAfterViewInit () {
|
||||
this.listItems.changes.subscribe(
|
||||
val => this.init.emit({ items: this.listItems })
|
||||
_ => this.init.emit({ items: this.listItems })
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,48 +5,50 @@ import { SafeHtml } from '@angular/platform-browser'
|
|||
@Pipe({ name: 'highlight' })
|
||||
export class HighlightPipe implements PipeTransform {
|
||||
/* use this for single match search */
|
||||
static SINGLE_MATCH: string = "Single-Match"
|
||||
static SINGLE_MATCH = 'Single-Match'
|
||||
/* use this for single match search with a restriction that target should start with search string */
|
||||
static SINGLE_AND_STARTS_WITH_MATCH: string = "Single-And-StartsWith-Match"
|
||||
static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match'
|
||||
/* use this for global search */
|
||||
static MULTI_MATCH: string = "Multi-Match"
|
||||
static MULTI_MATCH = 'Multi-Match'
|
||||
|
||||
constructor() {}
|
||||
transform(
|
||||
// tslint:disable-next-line:no-empty
|
||||
constructor () {}
|
||||
|
||||
transform (
|
||||
contentString: string = null,
|
||||
stringToHighlight: string = null,
|
||||
option: string = "Single-And-StartsWith-Match",
|
||||
caseSensitive: boolean = false,
|
||||
highlightStyleName: string = "search-highlight"
|
||||
option = 'Single-And-StartsWith-Match',
|
||||
caseSensitive = false,
|
||||
highlightStyleName = 'search-highlight'
|
||||
): SafeHtml {
|
||||
if (stringToHighlight && contentString && option) {
|
||||
let regex: any = ""
|
||||
let caseFlag: string = !caseSensitive ? "i" : ""
|
||||
switch (option) {
|
||||
case "Single-Match": {
|
||||
regex = new RegExp(stringToHighlight, caseFlag)
|
||||
break
|
||||
}
|
||||
case "Single-And-StartsWith-Match": {
|
||||
regex = new RegExp("^" + stringToHighlight, caseFlag)
|
||||
break
|
||||
}
|
||||
case "Multi-Match": {
|
||||
regex = new RegExp(stringToHighlight, "g" + caseFlag)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
// default will be a global case-insensitive match
|
||||
regex = new RegExp(stringToHighlight, "gi")
|
||||
}
|
||||
}
|
||||
const replaced = contentString.replace(
|
||||
regex,
|
||||
(match) => `<span class="${highlightStyleName}">${match}</span>`
|
||||
)
|
||||
return replaced
|
||||
} else {
|
||||
return contentString
|
||||
if (stringToHighlight && contentString && option) {
|
||||
let regex: any = ''
|
||||
const caseFlag: string = !caseSensitive ? 'i' : ''
|
||||
switch (option) {
|
||||
case 'Single-Match': {
|
||||
regex = new RegExp(stringToHighlight, caseFlag)
|
||||
break
|
||||
}
|
||||
case 'Single-And-StartsWith-Match': {
|
||||
regex = new RegExp("^" + stringToHighlight, caseFlag)
|
||||
break
|
||||
}
|
||||
case 'Multi-Match': {
|
||||
regex = new RegExp(stringToHighlight, 'g' + caseFlag)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
// default will be a global case-insensitive match
|
||||
regex = new RegExp(stringToHighlight, 'gi')
|
||||
}
|
||||
}
|
||||
const replaced = contentString.replace(
|
||||
regex,
|
||||
(match) => `<span class="${highlightStyleName}">${match}</span>`
|
||||
)
|
||||
return replaced
|
||||
} else {
|
||||
return contentString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
$icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
|
||||
|
||||
@import '_bootstrap-variables';
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
|
@ -234,7 +235,7 @@ table {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1600px) {
|
||||
@media screen and (max-width: #{map-get($grid-breakpoints, xxl)}) {
|
||||
.main-col {
|
||||
&.expanded {
|
||||
.margin-content {
|
||||
|
@ -245,7 +246,7 @@ table {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
@media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
|
||||
.main-col {
|
||||
&.expanded {
|
||||
.margin-content {
|
||||
|
|
|
@ -52,7 +52,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
|
|||
}
|
||||
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
@media screen and (min-width: #{map-get($grid-breakpoints, md)}) {
|
||||
.modal:before {
|
||||
vertical-align: middle;
|
||||
content: " ";
|
||||
|
|
|
@ -13,8 +13,9 @@ $grid-breakpoints: (
|
|||
md: 768px,
|
||||
// Large screen / desktop
|
||||
lg: 900px,
|
||||
// Extra large screen / wide desktop
|
||||
xl: 1200px
|
||||
// Extra large screens / wide desktops
|
||||
xl: 1200px,
|
||||
xxl: 1600px
|
||||
);
|
||||
|
||||
$container-max-widths: (
|
||||
|
|
|
@ -445,7 +445,6 @@
|
|||
@mixin actor-owner {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
display: inline-table;
|
||||
font-size: 13px;
|
||||
margin-top: 4px;
|
||||
color: var(--mainForegroundColor);
|
||||
|
@ -488,14 +487,15 @@
|
|||
.actor-names {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.actor-display-name {
|
||||
font-size: 23px;
|
||||
font-weight: $font-bold;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.actor-name {
|
||||
margin-left: 7px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 14px;
|
||||
|
@ -503,6 +503,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.actor-lower {
|
||||
grid-area: lower;
|
||||
}
|
||||
|
||||
.actor-followers {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
@ -522,6 +526,11 @@
|
|||
margin-bottom: 0;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 110%;
|
||||
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
font-size: 130%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import '_bootstrap-variables';
|
||||
|
||||
$small-view: 800px;
|
||||
$mobile-view: 500px;
|
||||
|
||||
|
|
Loading…
Reference in New Issue