File

app/sitesearch/shared/search-input/search-input.component.ts

Implements

OnInit OnDestroy

Metadata

changeDetection ChangeDetectionStrategy.OnPush
selector search-input
styleUrls search-input.component.scss
templateUrl search-input.component.html

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor(searchFactory: SearchFactory, loadingService: LoadingIndicatorService, activatedRoute: ActivatedRoute, router: Router, pubService: PublicationService, changeRef: ChangeDetectorRef, labelService: LabelService)
Parameters :
Name Type Optional
searchFactory SearchFactory No
loadingService LoadingIndicatorService No
activatedRoute ActivatedRoute No
router Router No
pubService PublicationService No
changeRef ChangeDetectorRef No
labelService LabelService No

Inputs

showSearchButton
Default value : true

Outputs

performedSearch
Type : EventEmitter

Methods

Public blurInput
blurInput()

Blur input box after timeout, to avoid query suggestion box from popping up after search has been made

Returns : void
clearInput
clearInput()
Returns : void
Public displaySuggestions
displaySuggestions()
Returns : void
Public doSearch
doSearch()

Perform search on current search box input value

Returns : void
Public focusInput
focusInput()

Manually put focus on search box

Returns : void
Public hideSuggestions
hideSuggestions()
Returns : void
Public highlightText
highlightText(fullText, highlightedText)

Highlight matching text between input and QS

Parameters :
Name Optional
fullText No
highlightedText No
Returns : any
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
Public searchBoxInput
searchBoxInput(event)

Handle key input on search box

Parameters :
Name Optional
event No
Returns : void
Public selectQs
selectQs(index)

Handles selection of a Query Suggestion

Parameters :
Name Optional
index No
Returns : void

Properties

Public activatedRoute
Type : ActivatedRoute
enableSearchButton
Type : boolean
input
Type : FormControl
Default value : new FormControl()
inputPlaceholder
Type : string
Default value : ''
Public loadingService
Type : LoadingIndicatorService
qcAvailable
Default value : false
search
Type : SearchService
searchBox
Decorators :
@ViewChild('searchBox')
searchButtonLabel
Type : string
Default value : ''
Public searchFactory
Type : SearchFactory
selectedQs
Default value : -1
showSuggestions
Default value : false
subscriptions
Type : []
Default value : []
suggestions
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>

search-input.component.scss

@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;
            }

        }
    }
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""