mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-07-31 17:38:28 +00:00
[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:
committed by
Denys Vuika
parent
ec2323b038
commit
f34c0a96a6
@@ -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,
|
||||
|
@@ -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>
|
@@ -0,0 +1,8 @@
|
||||
.adf-clear-search-icon-wrapper {
|
||||
width: 1em;
|
||||
|
||||
.mat-icon {
|
||||
font-size: 110%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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: []}} );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user