diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6a04ccbe1..d7536f5e4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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, diff --git a/src/app/components/search-input-control/search-input-control.component.html b/src/app/components/search-input-control/search-input-control.component.html new file mode 100644 index 000000000..4ef43ef9c --- /dev/null +++ b/src/app/components/search-input-control/search-input-control.component.html @@ -0,0 +1,83 @@ +
+
+ + + + +
+ clear + +
+
+
+
+ + + + + + + + + +

+ {{ item?.entry.name }} +

+ +

+
+

{{item?.entry.createdByUser.displayName}}

+
+ + + + +

{{ 'SEARCH.RESULTS.NONE' | translate:{searchTerm: searchTerm} }}

+
+
+
+
+
diff --git a/src/app/components/search-input-control/search-input-control.component.scss b/src/app/components/search-input-control/search-input-control.component.scss new file mode 100644 index 000000000..cb1dd538d --- /dev/null +++ b/src/app/components/search-input-control/search-input-control.component.scss @@ -0,0 +1,8 @@ +.adf-clear-search-icon-wrapper { + width: 1em; + + .mat-icon { + font-size: 110%; + cursor: pointer; + } +} diff --git a/src/app/components/search-input-control/search-input-control.component.ts b/src/app/components/search-input-control/search-input-control.component.ts new file mode 100644 index 000000000..496cb1d5b --- /dev/null +++ b/src/app/components/search-input-control/search-input-control.component.ts @@ -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 . + */ + +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 = 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 = new EventEmitter(); + + /** Emitted when a file item from the list of "find-as-you-type" results is selected. */ + @Output() + optionClicked: EventEmitter = new EventEmitter(); + + @ViewChild('search') + searchAutocomplete: SearchComponent; + + @ViewChild('searchInput') + searchInput: ElementRef; + + @ViewChildren(MatListItem) + private listResultElement: QueryList; + + @ContentChild(EmptySearchResultComponent) + emptySearchTemplate: EmptySearchResultComponent; + + searchTerm = ''; + subscriptAnimationState: string; + noSearchResultTemplate: TemplateRef = null; + skipToggle = false; + + private toggleSearch = new Subject(); + private focusSubject = new Subject(); + + 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 = this.listResultElement.first; + firstElement._getHostElement().focus(); + } + } + + onRowArrowDown($event: KeyboardEvent): void { + const nextElement: any = this.getNextElementSibling( $event.target); + if (nextElement) { + nextElement.focus(); + } + } + + onRowArrowUp($event: KeyboardEvent): void { + const previousElement: any = this.getPreviousElementSibling( $event.target); + if (previousElement) { + previousElement.focus(); + } else { + this.searchInput.nativeElement.focus(); + this.focusSubject.next(new FocusEvent('focus')); + } + } + + private setupFocusEventHandlers() { + const focusEvents: Observable = 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; + } + +} diff --git a/src/app/components/search-input/search-input.component.html b/src/app/components/search-input/search-input.component.html index 95fd9b69d..3b2ba7f96 100644 --- a/src/app/components/search-input/search-input.component.html +++ b/src/app/components/search-input/search-input.component.html @@ -1,15 +1,8 @@ - - - + diff --git a/src/app/components/search-input/search-input.component.ts b/src/app/components/search-input/search-input.component.ts index 860d87d5e..ad8455fcc 100644 --- a/src/app/components/search-input/search-input.component.ts +++ b/src/app/components/search-input/search-input.component.ts @@ -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) { + } + + 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.store.dispatch(new SearchByTermAction(searchTerm)); this.hasOneChange = false; }, 1000); } diff --git a/src/app/components/search/search.component.ts b/src/app/components/search/search.component.ts index 8773b5514..dca6a02c4 100644 --- a/src/app/components/search/search.component.ts +++ b/src/app/components/search/search.component.ts @@ -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: []}} ); } }); }