[ACA-1451] search input component (#432)

* made copy of ADF search-control component

* show search icon even if not 'expandable'

* clear button

* no autoComplete triggered calls when liveSearchEnabled is false

* remove unneeded arguments
This commit is contained in:
Suzana Dirla
2018-06-19 16:07:44 +03:00
committed by Denys Vuika
parent ec2323b038
commit f34c0a96a6
7 changed files with 389 additions and 25 deletions

View File

@@ -49,6 +49,7 @@ import { SidenavViewsManagerDirective } from './components/layout/sidenav-views-
import { HeaderComponent } from './components/header/header.component';
import { CurrentUserComponent } from './components/current-user/current-user.component';
import { SearchInputComponent } from './components/search-input/search-input.component';
import { SearchInputControlComponent } from './components/search-input-control/search-input-control.component';
import { SidenavComponent } from './components/sidenav/sidenav.component';
import { AboutComponent } from './components/about/about.component';
import { LocationLinkComponent } from './components/location-link/location-link.component';
@@ -108,6 +109,7 @@ import { AppStoreModule } from './store/app-store.module';
HeaderComponent,
CurrentUserComponent,
SearchInputComponent,
SearchInputControlComponent,
SidenavComponent,
FilesComponent,
FavoritesComponent,

View File

@@ -0,0 +1,83 @@
<div class="adf-search-container">
<div [@transitionMessages]="subscriptAnimationState" (@transitionMessages.done)="applySearchFocus($event)">
<button mat-icon-button
id="adf-search-button"
class="adf-search-button"
[title]="'SEARCH.BUTTON.TOOLTIP' | translate"
(click)="expandable && toggleSearchBar()"
(keyup.enter)="expandable && toggleSearchBar()">
<mat-icon [attr.aria-label]="'SEARCH.BUTTON.ARIA-LABEL' | translate">search</mat-icon>
</button>
<mat-form-field class="adf-input-form-field-divider">
<input matInput #searchInput
[attr.aria-label]="'SEARCH.INPUT.ARIA-LABEL' | translate"
[type]="inputType"
[autocomplete]="getAutoComplete()"
id="adf-control-input"
[(ngModel)]="searchTerm"
(focus)="activateToolbar()"
(blur)="onBlur($event)"
(keyup.escape)="toggleSearchBar()"
(keyup.arrowdown)="selectFirstResult()"
(ngModelChange)="inputChange($event)"
[searchAutocomplete]="liveSearchEnabled && auto"
(keyup.enter)="searchSubmit($event)">
<div matSuffix class="adf-clear-search-icon-wrapper">
<mat-icon *ngIf="searchTerm.length > 0"
(click)="clear()">clear
</mat-icon>
</div>
</mat-form-field>
</div>
</div>
<adf-search #search
#auto="searchAutocomplete"
class="adf-search-result-autocomplete"
[maxResults]="liveSearchMaxResults"
[queryBody]="customQueryBody">
<ng-template let-data>
<mat-list *ngIf="isSearchBarActive()" id="autocomplete-search-result-list">
<mat-list-item
*ngFor="let item of data?.list?.entries; let idx = index"
id="result_option_{{idx}}"
[attr.data-automation-id]="'autocomplete_for_' + item.entry.name"
[tabindex]="0"
(focus)="onFocus($event)"
(blur)="onBlur($event)"
(keyup.arrowdown)="onRowArrowDown($event)"
(keyup.arrowup)="onRowArrowUp($event)"
class="adf-search-autocomplete-item"
(click)="elementClicked(item)"
(keyup.enter)="elementClicked(item)"
(touchend)="elementClicked(item)">
<!-- This is a comment -->
<mat-icon mat-list-icon>
<img [src]="getMimeTypeIcon(item)" />
</mat-icon>
<h4 mat-line id="result_name_{{idx}}"
*ngIf="highlight; else elseBlock"
class="adf-search-fixed-text"
[innerHtml]="item.entry.name | highlight: searchTerm">
{{ item?.entry.name }}
</h4>
<ng-template #elseBlock>
<h4 class="adf-search-fixed-text" mat-line id="result_name_{{idx}}" [innerHtml]="item.entry.name"></h4>
</ng-template>
<p mat-line class="adf-search-fixed-text"> {{item?.entry.createdByUser.displayName}} </p>
</mat-list-item>
<mat-list-item id="search_no_result"
data-automation-id="search_no_result_found"
*ngIf="data?.list?.entries.length === 0">
<ng-content
selector="adf-empty-search-result"
*ngIf="isNoSearchTemplatePresent() else defaultNoResult">
</ng-content>
<ng-template #defaultNoResult>
<p mat-line class="adf-search-fixed-text">{{ 'SEARCH.RESULTS.NONE' | translate:{searchTerm: searchTerm} }}</p>
</ng-template>
</mat-list-item>
</mat-list>
</ng-template>
</adf-search>

View File

@@ -0,0 +1,8 @@
.adf-clear-search-icon-wrapper {
width: 1em;
.mat-icon {
font-size: 110%;
cursor: pointer;
}
}

View File

@@ -0,0 +1,275 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { ThumbnailService } from '@alfresco/adf-core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output,
QueryList, ViewEncapsulation, ViewChild, ViewChildren, ElementRef, TemplateRef, ContentChild } from '@angular/core';
import { MinimalNodeEntity, QueryBody } from 'alfresco-js-api';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { MatListItem } from '@angular/material';
import { debounceTime } from 'rxjs/operators';
import { EmptySearchResultComponent, SearchComponent } from '@alfresco/adf-content-services';
@Component({
selector: 'app-search-input-control',
templateUrl: './search-input-control.component.html',
styleUrls: ['./search-input-control.component.scss'],
animations: [
trigger('transitionMessages', [
state('active', style({ transform: 'translateX(0%)', 'margin-left': '13px' })),
state('inactive', style({ transform: 'translateX(81%)'})),
state('no-animation', style({ transform: 'translateX(0%)', width: '100%' })),
transition('inactive => active',
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')),
transition('active => inactive',
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)'))
])
],
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-search-control' }
})
export class SearchInputControlComponent implements OnInit, OnDestroy {
/** Toggles whether to use an expanding search control. If false
* then a regular input is used.
*/
@Input()
expandable = true;
/** Toggles highlighting of the search term in the results. */
@Input()
highlight = false;
/** Type of the input field to render, e.g. "search" or "text" (default). */
@Input()
inputType = 'text';
/** Toggles auto-completion of the search input field. */
@Input()
autocomplete = false;
/** Toggles "find-as-you-type" suggestions for possible matches. */
@Input()
liveSearchEnabled = true;
/** Maximum number of results to show in the live search. */
@Input()
liveSearchMaxResults = 5;
/** @deprecated in 2.1.0 */
@Input()
customQueryBody: QueryBody;
/** Emitted when the search is submitted pressing ENTER button.
* The search term is provided as value of the event.
*/
@Output()
submit: EventEmitter<any> = new EventEmitter();
/** Emitted when the search term is changed. The search term is provided
* in the 'value' property of the returned object. If the term is less
* than three characters in length then the term is truncated to an empty
* string.
*/
@Output()
searchChange: EventEmitter<string> = new EventEmitter();
/** Emitted when a file item from the list of "find-as-you-type" results is selected. */
@Output()
optionClicked: EventEmitter<any> = new EventEmitter();
@ViewChild('search')
searchAutocomplete: SearchComponent;
@ViewChild('searchInput')
searchInput: ElementRef;
@ViewChildren(MatListItem)
private listResultElement: QueryList<MatListItem>;
@ContentChild(EmptySearchResultComponent)
emptySearchTemplate: EmptySearchResultComponent;
searchTerm = '';
subscriptAnimationState: string;
noSearchResultTemplate: TemplateRef <any> = null;
skipToggle = false;
private toggleSearch = new Subject<any>();
private focusSubject = new Subject<FocusEvent>();
constructor(private thumbnailService: ThumbnailService) {
this.toggleSearch.asObservable().pipe(debounceTime(200)).subscribe(() => {
if (this.expandable && !this.skipToggle) {
this.subscriptAnimationState = this.subscriptAnimationState === 'inactive' ? 'active' : 'inactive';
if (this.subscriptAnimationState === 'inactive') {
this.searchTerm = '';
this.searchAutocomplete.resetResults();
if ( document.activeElement.id === this.searchInput.nativeElement.id) {
this.searchInput.nativeElement.blur();
}
}
}
this.skipToggle = false;
});
}
applySearchFocus(animationDoneEvent) {
if (animationDoneEvent.toState === 'active') {
this.searchInput.nativeElement.focus();
}
}
ngOnInit() {
this.subscriptAnimationState = this.expandable ? 'inactive' : 'no-animation';
this.setupFocusEventHandlers();
}
isNoSearchTemplatePresent(): boolean {
return this.emptySearchTemplate ? true : false;
}
ngOnDestroy(): void {
if (this.focusSubject) {
this.focusSubject.unsubscribe();
this.focusSubject = null;
}
if (this.toggleSearch) {
this.toggleSearch.unsubscribe();
this.toggleSearch = null;
}
}
searchSubmit(event: any) {
this.submit.emit(event);
this.toggleSearchBar();
}
inputChange(event: any) {
this.searchChange.emit(event);
}
getAutoComplete(): string {
return this.autocomplete ? 'on' : 'off';
}
getMimeTypeIcon(node: MinimalNodeEntity): string {
let mimeType;
if (node.entry.content && node.entry.content.mimeType) {
mimeType = node.entry.content.mimeType;
}
if (node.entry.isFolder) {
mimeType = 'folder';
}
return this.thumbnailService.getMimeTypeIcon(mimeType);
}
isSearchBarActive() {
return this.subscriptAnimationState === 'active' && this.liveSearchEnabled;
}
toggleSearchBar() {
if (this.toggleSearch) {
this.toggleSearch.next();
}
}
elementClicked(item: any) {
if (item.entry) {
this.optionClicked.next(item);
this.toggleSearchBar();
}
}
onFocus($event): void {
this.focusSubject.next($event);
}
onBlur($event): void {
this.focusSubject.next($event);
}
activateToolbar() {
if (!this.isSearchBarActive()) {
this.toggleSearchBar();
}
}
selectFirstResult() {
if ( this.listResultElement && this.listResultElement.length > 0) {
const firstElement: MatListItem = <MatListItem> this.listResultElement.first;
firstElement._getHostElement().focus();
}
}
onRowArrowDown($event: KeyboardEvent): void {
const nextElement: any = this.getNextElementSibling(<Element> $event.target);
if (nextElement) {
nextElement.focus();
}
}
onRowArrowUp($event: KeyboardEvent): void {
const previousElement: any = this.getPreviousElementSibling(<Element> $event.target);
if (previousElement) {
previousElement.focus();
} else {
this.searchInput.nativeElement.focus();
this.focusSubject.next(new FocusEvent('focus'));
}
}
private setupFocusEventHandlers() {
const focusEvents: Observable<FocusEvent> = this.focusSubject.asObservable()
.debounceTime(50);
focusEvents.filter(($event: any) => {
return this.isSearchBarActive() && ($event.type === 'blur' || $event.type === 'focusout');
}).subscribe(() => {
this.toggleSearchBar();
});
}
clear(event: any) {
this.searchTerm = '';
this.searchChange.emit('');
this.skipToggle = true;
}
private getNextElementSibling(node: Element): Element {
return node.nextElementSibling;
}
private getPreviousElementSibling(node: Element): Element {
return node.previousElementSibling;
}
}

View File

@@ -1,15 +1,8 @@
<button mat-icon-button
*ngIf="onSearchResults"
id="adf-search-button"
class="adf-search-button"
[title]="'SEARCH.BUTTON.TOOLTIP' | translate">
<mat-icon [attr.aria-label]="'SEARCH.BUTTON.ARIA-LABEL' | translate">search</mat-icon>
</button>
<adf-search-control #searchControl
<app-search-input-control #searchInputControl
[highlight]="true"
(optionClicked)="onItemClicked($event)"
[expandable]="!onSearchResults"
[liveSearchEnabled]="!onSearchResults"
(submit)="onSearchSubmit($event)"
(searchChange)="onSearchChange($event)">
</adf-search-control>
</app-search-input-control>

View File

@@ -29,7 +29,7 @@ import {
UrlTree
} from '@angular/router';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { SearchControlComponent } from '@alfresco/adf-content-services';
import { SearchInputControlComponent } from '../search-input-control/search-input-control.component';
import { Store } from '@ngrx/store';
import { AppStore } from '../../store/states/app.state';
import { SearchByTermAction, ViewNodeAction, NavigateToFolder } from '../../store/actions';
@@ -46,10 +46,15 @@ export class SearchInputComponent implements OnInit {
hasNewChange = false;
navigationTimer: any;
@ViewChild('searchControl')
searchControl: SearchControlComponent;
@ViewChild('searchInputControl')
searchInputControl: SearchInputControlComponent;
constructor(private router: Router, private store: Store<AppStore>) {
}
ngOnInit() {
this.showInputValue();
this.router.events.filter(e => e instanceof RouterEvent).subscribe(event => {
if (event instanceof NavigationEnd) {
this.showInputValue();
@@ -57,10 +62,6 @@ export class SearchInputComponent implements OnInit {
});
}
ngOnInit() {
this.showInputValue();
}
showInputValue() {
if (this.onSearchResults) {
@@ -73,14 +74,16 @@ export class SearchInputComponent implements OnInit {
searchedWord = urlSegments[0].parameters['q'];
}
this.searchControl.searchTerm = searchedWord;
this.searchControl.subscriptAnimationState = 'no-animation';
if (this.searchInputControl) {
this.searchInputControl.searchTerm = searchedWord;
this.searchInputControl.subscriptAnimationState = 'no-animation';
}
} else {
if (this.searchControl.subscriptAnimationState === 'no-animation') {
this.searchControl.subscriptAnimationState = 'active';
this.searchControl.searchTerm = '';
this.searchControl.toggleSearchBar();
if (this.searchInputControl.subscriptAnimationState === 'no-animation') {
this.searchInputControl.subscriptAnimationState = 'active';
this.searchInputControl.searchTerm = '';
this.searchInputControl.toggleSearchBar();
}
}
}
@@ -127,9 +130,7 @@ export class SearchInputComponent implements OnInit {
}
this.navigationTimer = setTimeout(() => {
if (searchTerm) {
this.store.dispatch(new SearchByTermAction(searchTerm));
}
this.hasOneChange = false;
}, 1000);
}

View File

@@ -87,6 +87,8 @@ export class SearchComponent extends PageComponent implements OnInit {
if (query) {
this.queryBuilder.userQuery = query;
this.queryBuilder.update();
} else {
this.onSearchResultLoaded( {list: { pagination: { totalItems: 0 }, entries: []}} );
}
});
}