import { Component, OnInit, ViewChild, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, Output, EventEmitter } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { map, debounceTime } from 'rxjs/operators';
import { SearchFactory, SearchService } from 'skf-search-angular-service';
import { LoadingIndicatorService } from 'src/app/core/services/loading-indicator.service';
import { PublicationService } from 'src/app/core/services/publication-service/publication.service';
import { LabelService } from 'src/app/core/services/label-service/label-service.service';
import { AppConfig } from '../../../app.config';
@Component ({
selector: 'search-input',
templateUrl: 'search-input.component.html',
styleUrls: ['search-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchInputComponent implements OnInit, OnDestroy {
@Input() showSearchButton = true;
@Output() performedSearch = new EventEmitter<boolean>();
enableSearchButton: boolean;
input: FormControl = new FormControl();
suggestions;
qcAvailable = false;
showSuggestions = false;
selectedQs = -1;
subscriptions = [];
search: SearchService;
// Labels
inputPlaceholder = '';
searchButtonLabel = '';
@ViewChild('searchBox') searchBox;
constructor(
public searchFactory: SearchFactory,
public loadingService: LoadingIndicatorService,
public activatedRoute: ActivatedRoute,
private router: Router,
private pubService: PublicationService,
private changeRef: ChangeDetectorRef,
private labelService: LabelService
) {
this.search = this.searchFactory.get(`site-search-v2-${AppConfig.settings.searchEnvironment.name}`);
}
ngOnInit() {
this.labelService.getLabel('searchBoxText').then(res => {
this.inputPlaceholder = res;
this.changeRef.detectChanges();
});
this.labelService.getLabel('searchBoxButton').then(res => {
this.searchButtonLabel = res;
this.changeRef.detectChanges();
});
// Subscribe to changes to query parameters in the url.
this.subscriptions.push(this.activatedRoute.queryParams.subscribe(params => {
if ('q' in params) {
if (params.q === '*') { this.clearInput(); } // * searches for everything, shouldn't be displayed in input.
else { this.input.setValue(params.q, {emitEvent: false}); } // Set emitEvent to false to prevent query suggestions from being triggered.
}
else { this.clearInput(); }
}));
const lang = this.pubService.getLanguage();
const site = this.pubService.getPublicationId();
// Subscribe to changes in the search box input
this.subscriptions.push( this.input.valueChanges.pipe(
debounceTime(200),
map(query => query.trim())
).subscribe((query: string): void => {
if (this.input.value.length >= 2) { // Search for query suggestion when user input 2 chars or more
const qc = 'ps'; // Change searcher to branded-sites-qc for branded sites
this.search.doSuggest({ queryString: query, qc, language: lang, brand: 'skf', site, system: 'metric'});
}
if (this.input.value.length === 0) { this.qcAvailable = false; }
}));
// Subscribe to when query suggestions are received after doSuggest runs.
this.subscriptions.push( this.search.suggestions.subscribe(suggestions => {
this.suggestions = suggestions;
this.qcAvailable = true;
this.changeRef.detectChanges();
},
error => { console.log('ERROR: Suggestions result error'); }
));
}
ngOnDestroy() {
// Called once, before the instance is destroyed. Unsubscribe all subscriptions to prevent memory leeks.
this.subscriptions.forEach(sub => {
if (sub) { sub.unsubscribe(); }
});
}
/** Perform search on current search box input value */
public doSearch() {
this.loadingService.show();
const pubPath = this.pubService.getPublicationPath();
const sitesearchPage = `${pubPath}/search-results`;
const currentUrl = `${this.activatedRoute.snapshot.url.join('/')}`;
// If not on Site Search page, route to it.
if (currentUrl !== sitesearchPage) {
this.router.navigateByUrl(`${pubPath}/search-results?q=${this.input.value}`);
} else {
if (this.input.value.length > 0) {
this.search.doSearch({queryString: this.input.value});
} else { // If empty search box
this.search.doSearch({queryString: '*'});
}
}
this.blurInput();
this.hideSuggestions();
this.performedSearch.emit(true);
}
/** Handles selection of a Query Suggestion */
public selectQs(index): void {
// If a Query Suggestion is selected, search for it if the query is complete.
if (index >= 0) {
if (!this.suggestions[index].incomplete) {
const pubPath = this.pubService.getPublicationPath();
const sitesearchPage = `${pubPath}/search-results`;
this.performedSearch.emit(true);
// If not on Site Search page, reroute there
if (sitesearchPage !== this.router.url.split('?')[0]) {
this.router.navigateByUrl(`${pubPath}/search-results?q=${this.suggestions[index].fullText}`);
} else {
this.loadingService.show();
this.suggestions[index].select();
this.hideSuggestions();
this.blurInput();
this.input.setValue(this.suggestions[index].fullText);
this.selectedQs = -1;
}
} else { // If the search query is incomplete (e.g ..with diameter of..), then don't do a search until it's complete.
this.input.setValue(this.suggestions[index].fullText);
this.selectedQs = -1;
this.focusInput();
}
} else { // index is -1 if no query suggestion has been selected.
this.doSearch();
}
}
/** Highlight matching text between input and QS */
public highlightText(fullText, highlightedText) {
try {
return fullText.replace(new RegExp(highlightedText, 'gi'), match => {
return '<span class="qsHighlight">' + match + '</span>';
});
} catch (e) {
return fullText;
}
}
/** Blur input box after timeout, to avoid query suggestion box from popping up after search has been made */
public blurInput(): void {
this.searchBox.nativeElement.blur();
this.changeRef.detectChanges();
}
public hideSuggestions(): void {
this.showSuggestions = false;
}
public displaySuggestions(): void {
this.showSuggestions = true;
}
/** Manually put focus on search box */
public focusInput() {
this.searchBox.nativeElement.focus();
this.changeRef.detectChanges();
}
/** Handle key input on search box */
public searchBoxInput(event) {
if (event.code) {
switch (event.code) {
case 'ArrowDown': {
event.preventDefault();
if (this.suggestions && this.suggestions.length - 1 < this.selectedQs + 1) { this.selectedQs = 0; }
else { this.selectedQs = this.selectedQs + 1; }
// Set input box string to the QS which has been navigated to
this.input.setValue(this.suggestions[this.selectedQs].fullText, {emitEvent: false});
break;
} case 'ArrowUp': {
event.preventDefault();
if ( this.suggestions && this.selectedQs - 1 < -1) { this.selectedQs = this.suggestions.length - 1; }
else { this.selectedQs = this.selectedQs - 1; }
// Set input box string to the QS which has been navigated to
if (this.selectedQs > -1) { this.input.setValue(this.suggestions[this.selectedQs].fullText, {emitEvent: false}); }
break;
} case 'Escape': {
this.selectedQs = -1;
this.qcAvailable = false;
break;
} case 'Enter': {
this.selectQs(this.selectedQs);
break;
} case 'NumpadEnter': {
this.selectQs(this.selectedQs);
break;
} default: {
this.selectedQs = -1;
}
}
} else {
// IE has different events, handled here.
switch (event.key) {
case 'Down': {
if (this.suggestions && this.suggestions.length - 1 < this.selectedQs + 1) { this.selectedQs = 0; }
else { this.selectedQs = this.selectedQs + 1; }
break;
} case 'Up': {
if (this.suggestions && this.selectedQs - 1 < -1) { this.selectedQs = this.suggestions.length - 1; }
else { this.selectedQs = this.selectedQs - 1; }
break;
} case 'Enter': {
this.selectQs(this.selectedQs);
break;
} case 'Esc': {
this.selectedQs = -1;
this.qcAvailable = false;
break;
} default: {
this.selectedQs = -1;
}
}
}
}
clearInput() {
this.input.setValue('');
}
}
<form class="searchbox" aria-label="search-input">
<input #searchBox [placeholder]="inputPlaceholder" class="search-textbox" (keydown)="searchBoxInput($event)"
(focus)="displaySuggestions()" [formControl]="input" type="search">
<div class="clear-input" *ngIf="input.value.length > 0" (click)="clearInput()">
<i class="fas fa-times-circle" aria-hidden="true"></i>
</div>
<div class="btn btn-green search-button" (click)="doSearch()" *ngIf="showSearchButton">
<span>{{searchButtonLabel | uppercase}}</span>
<i class="icon-masthead-search"></i>
</div>
<div class="qc-box" *ngIf="showSuggestions && qcAvailable && suggestions?.length > 0"
(clickOutside)="hideSuggestions()" [exclude]="'.search-textbox'" [delayClickOutsideInit]="true">
<ul class="typeahead-list">
<li *ngFor="let suggestion of suggestions; let i = index;" [ngClass]="{'selected': i==selectedQs}"
(mousedown)="selectQs(i)" (mouseover)="selectedQs=-1">
<div class="suggestion" [innerHTML]="highlightText(suggestion.displayName, input.value)"></div>
</li>
</ul>
</div>
</form>
@import "src/app/styles/helpers";
:host {
width: 100%;
}
/* clears the 'X' from Internet Explorer */
input[type=search]::-ms-clear { display: none; width : 0; height: 0; }
input[type=search]::-ms-reveal { display: none; width : 0; height: 0; }
/* clears the 'X' from Chrome */
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration { display: none; }
.searchbox {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
max-width: calc-rem(950);
margin: 0 auto;
background-color: $cool-grey;
.search-textbox {
width: 70%;
padding: calc-rem(18) calc-rem(23);
background-color: $cool-grey;
@include font-size(18);
font-family: 'Circular Black';
border: none;
color: white;
@include media-breakpoint-down(sm) {
width: 90%;
height: calc-rem(50);
}
&:focus {
outline: none;
}
&::placeholder {
color: #C8CDD0;
}
// IE compatibility
&::-ms-clear {
display: none;
}
}
.clear-input {
position: absolute;
right: calc-rem(200);
cursor: pointer;
@include media-breakpoint-down(sm) {
right: calc-rem(10);
}
&:hover {
i {
color: white;
opacity: 0.8;
}
}
i {
color: $light-blue;
opacity: 0.5;
font-size: calc-rem(22);
}
}
.search-button {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
right: calc-rem(12);
top: calc-rem(12);
bottom: calc-rem(12);
padding: 0px calc-rem(40);
cursor: pointer;
@include media-breakpoint-down(sm) {
right: 0;
left: 0;
top: calc(100% + 10px);
bottom: unset;
padding: calc-rem(13) calc-rem(40);
height: calc-rem(50);
}
i {
color: white;
margin-left: calc-rem(5);
line-height: 0;
@include font-size(20);
}
}
.qc-box {
position: absolute;
z-index: 999;
top: 100%;
left: 0;
width: 100%;
outline: none;
background-color: white;
max-height: calc-rem(500);
border: 1px solid $light-slate;
overflow: auto;
.typeahead-list {
list-style-type: none;
margin: 0;
padding: 0;
li {
text-align: left;
cursor: pointer;
user-select: none;
padding: calc-rem(2);
border-bottom: 1px solid whitesmoke;
color: grey;
::ng-deep .qsHighlight { // Used to make highlight matching word between input and search suggestions
font-weight: 700;
}
&:hover {
background-color: $blue;
color: white;
}
.suggestion {
padding: calc-rem(10) calc-rem(20);
}
}
.selected {
background-color: $blue;
color: white;
}
}
}
}