/**
* @license
* Copyright Akveo. All Rights Reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { of as observableOf, Subject } from 'rxjs';
import { filter, delay, takeUntil } from 'rxjs/operators';
import { NbSearchService } from './search.service';
import { NbThemeService } from '../../services/theme.service';
import { NbOverlayService } from '../cdk/overlay/overlay-service';
import { NbOverlayRef, NbPortalDirective } from '../cdk/overlay/mapping';
/**
* search-field-component is used under the hood by nb-search component
* can't be used itself
*/
@Component({
selector: 'nb-search-field',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: [
'styles/search.component.modal-zoomin.scss',
'styles/search.component.layout-rotate.scss',
'styles/search.component.modal-move.scss',
'styles/search.component.curtain.scss',
'styles/search.component.column-curtain.scss',
'styles/search.component.modal-drop.scss',
'styles/search.component.modal-half.scss',
],
template: `
`,
})
export class NbSearchFieldComponent implements OnChanges, AfterViewInit {
static readonly TYPE_MODAL_ZOOMIN = 'modal-zoomin';
static readonly TYPE_ROTATE_LAYOUT = 'rotate-layout';
static readonly TYPE_MODAL_MOVE = 'modal-move';
static readonly TYPE_CURTAIN = 'curtain';
static readonly TYPE_COLUMN_CURTAIN = 'column-curtain';
static readonly TYPE_MODAL_DROP = 'modal-drop';
static readonly TYPE_MODAL_HALF = 'modal-half';
@Input() type: string;
@Input() placeholder: string;
@Input() hint: string;
@Input() show = false;
@Output() close = new EventEmitter();
@Output() search = new EventEmitter();
@Output() searchInput = new EventEmitter();
@ViewChild('searchInput') inputElement: ElementRef;
@HostBinding('class.show')
get showClass() {
return this.show;
}
@HostBinding('class.modal-zoomin')
get modalZoomin() {
return this.type === NbSearchFieldComponent.TYPE_MODAL_ZOOMIN;
}
@HostBinding('class.rotate-layout')
get rotateLayout() {
return this.type === NbSearchFieldComponent.TYPE_ROTATE_LAYOUT;
}
@HostBinding('class.modal-move')
get modalMove() {
return this.type === NbSearchFieldComponent.TYPE_MODAL_MOVE;
}
@HostBinding('class.curtain')
get curtain() {
return this.type === NbSearchFieldComponent.TYPE_CURTAIN;
}
@HostBinding('class.column-curtain')
get columnCurtain() {
return this.type === NbSearchFieldComponent.TYPE_COLUMN_CURTAIN;
}
@HostBinding('class.modal-drop')
get modalDrop() {
return this.type === NbSearchFieldComponent.TYPE_MODAL_DROP;
}
@HostBinding('class.modal-half')
get modalHalf() {
return this.type === NbSearchFieldComponent.TYPE_MODAL_HALF;
}
ngOnChanges({ show }: SimpleChanges) {
const becameHidden = !show.isFirstChange() && show.currentValue === false;
if (becameHidden && this.inputElement) {
this.inputElement.nativeElement.value = '';
}
this.focusInput();
}
ngAfterViewInit() {
this.focusInput();
}
emitClose() {
this.close.emit();
}
submitSearch(term) {
if (term) {
this.search.emit(term);
}
}
emitSearchInput(term: string) {
this.searchInput.emit(term);
}
focusInput() {
if (this.show && this.inputElement) {
this.inputElement.nativeElement.focus();
}
}
}
export type NbSearchType = 'modal-zoomin' | 'rotate-layout' | 'modal-move' |
'curtain' | 'column-curtain' | 'modal-drop' | 'modal-half';
/**
* Beautiful full-page search control.
*
* @stacked-example(Showcase, search/search-showcase.component)
*
* Basic setup:
*
* ```ts
*
* ```
* ### Installation
*
* Import `NbSearchModule` to your feature module.
* ```ts
* @NgModule({
* imports: [
* // ...
* NbSearchModule,
* ],
* })
* export class PageModule { }
* ```
* ### Usage
*
* Several animation types are available:
* modal-zoomin, rotate-layout, modal-move, curtain, column-curtain, modal-drop, modal-half
*
* It is also possible to handle search event using `NbSearchService`:
*
* @stacked-example(Search Event, search/search-event.component)
*
* @styles
*
* search-background-color:
* search-divider-color:
* search-divider-style:
* search-divider-width:
* search-extra-background-color:
* search-text-color:
* search-text-font-family:
* search-text-font-size:
* search-text-font-weight:
* search-text-line-height:
* search-placeholder-text-color:
* search-info-text-color:
* search-info-text-font-family:
* search-info-text-font-size:
* search-info-text-font-weight:
* search-info-text-line-height:
*/
@Component({
selector: 'nb-search',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['styles/search.component.scss'],
template: `
`,
})
export class NbSearchComponent implements OnInit, OnDestroy {
private destroy$ = new Subject();
private overlayRef: NbOverlayRef;
showSearchField = false;
/**
* Tags a search with some ID, can be later used in the search service
* to determine which search component triggered the action, if multiple searches exist on the page.
*
* @type {string}
*/
@Input() tag: string;
/**
* Search input placeholder
* @type {string}
*/
@Input() placeholder: string = 'Search...';
/**
* Hint showing under the input field to improve user experience
*
* @type {string}
*/
@Input() hint: string = 'Hit enter to search';
/**
* Search design type, available types are
* modal-zoomin, rotate-layout, modal-move, curtain, column-curtain, modal-drop, modal-half
* @type {string}
*/
@Input() type: NbSearchType;
@ViewChild(NbPortalDirective) searchFieldPortal: NbPortalDirective;
@ViewChild('searchButton', { read: ElementRef }) searchButton: ElementRef;
constructor(
private searchService: NbSearchService,
private themeService: NbThemeService,
private router: Router,
private overlayService: NbOverlayService,
private changeDetector: ChangeDetectorRef,
) {}
ngOnInit() {
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.destroy$),
)
.subscribe(() => this.hideSearch());
this.searchService.onSearchActivate()
.pipe(
filter(data => !this.tag || data.tag === this.tag),
takeUntil(this.destroy$),
)
.subscribe(() => this.openSearch());
this.searchService.onSearchDeactivate()
.pipe(
filter(data => !this.tag || data.tag === this.tag),
takeUntil(this.destroy$),
)
.subscribe(() => this.hideSearch());
}
ngOnDestroy() {
if (this.overlayRef && this.overlayRef.hasAttached()) {
this.removeLayoutClasses();
this.overlayRef.detach();
}
this.destroy$.next();
this.destroy$.complete();
}
openSearch() {
if (!this.overlayRef) {
this.overlayRef = this.overlayService.create();
this.overlayRef.attach(this.searchFieldPortal);
}
this.themeService.appendLayoutClass(this.type);
observableOf(null).pipe(delay(0)).subscribe(() => {
this.themeService.appendLayoutClass('with-search');
this.showSearchField = true;
this.changeDetector.detectChanges();
});
}
hideSearch() {
this.removeLayoutClasses();
this.showSearchField = false;
this.changeDetector.detectChanges();
this.searchButton.nativeElement.focus();
}
search(term) {
this.searchService.submitSearch(term, this.tag);
this.hideSearch();
}
emitInput(term: string) {
this.searchService.searchInput(term, this.tag);
}
emitActivate() {
this.searchService.activateSearch(this.type, this.tag);
}
emitDeactivate() {
this.searchService.deactivateSearch(this.type, this.tag);
}
private removeLayoutClasses() {
this.themeService.removeLayoutClass('with-search');
observableOf(null).pipe(delay(500)).subscribe(() => {
this.themeService.removeLayoutClass(this.type);
});
}
}