diff --git a/docs/core/components/search-cloud.component.md b/docs/core/components/search-cloud.component.md deleted file mode 100644 index b953d245d3..0000000000 --- a/docs/core/components/search-cloud.component.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -Title: Search Cloud Component -Added: v3.5.0 -Status: Active -Last reviewed: 2019-10-24 ---- - -# [Search Cloud Component](../../../lib/core/search-cloud/search-cloud.component.ts "Defined in pagination.component.ts") - -Should manage search for cloud components - -## Basic Usage - -```html - - [type]="'text'" - [placeholder]="'placeholder'" - [debounceTime]="200" - [expandable]='false' - (change)="onSearchValueChanged($event)" - -``` - -## Class members - -### Properties - -| Name | Type | Default value | Description | -| ---- | ---- | ------------- | ----------- | -| type | [`SearchCloudTypesEnum`](../../../lib/core/models/search-cloud.model.ts) | | search type ('text'). | -| value | `string` | | preselected input value | -| expandable | `boolean` | false | The field should expand on click when this flag is true | -| placeholder | `string` | | placeholder content. | -| debounceTime | `number` | 500 | Time in miliseconds for debounce the event. | - -### Events - -| Name | Type | Description | -| ---- | ---- | ----------- | -| change | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when search widget value is changed. | diff --git a/docs/core/components/search-text-input.component.md b/docs/core/components/search-text-input.component.md new file mode 100644 index 0000000000..004f42bf74 --- /dev/null +++ b/docs/core/components/search-text-input.component.md @@ -0,0 +1,50 @@ +--- +Title: Search Text Input Component +Added: v3.6.0 +Status: Active +Last reviewed: 2019-11-06 +--- + +# [Search Text Input Component](../../../lib/core/search-text/search-text-input.component.ts "Defined in search-text-input.component.ts") + +Displays a input text that supports autocompletion + + + +## Basic Usage + +```html + + +``` + +## Class members + +### Properties + +| Name | Type | Default value | Description | +| ---- | ---- | ------------- | ----------- | +| autocomplete | `boolean` | false | Toggles auto-completion of the search input field. | +| expandable | `boolean` | true | Toggles whether to use an expanding search control. If false then a regular input is used. | +| highlight | `boolean` | false | Toggles highlighting of the search term in the results. | +| inputType | `string` | "text" | Type of the input field to render, e.g. "search" or "text" (default). | +| liveSearchEnabled | `boolean` | true | Toggles "find-as-you-type" suggestions for possible matches. | +| searchAutocomplete | [`SearchTriggerDirective`](../../../lib/core/search-text/search-trigger.directive.ts) | null | Trigger autocomplete results on input change | +| searchTerm | `string` | empty | Preselected search widget value | +| debounceTime | `number` | 0 | Debounce time in miliseconds | +| focusListener | [`Observable`](http://reactivex.io/documentation/observable.html) `<` [`FocusEvent`](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent) `>` | null | Listener for results-list events (focus, blur and focusout) | +| defaultState | [`SearchTextStateEnum`](../../../lib/core/models/search-text-input.model.ts) | collapsed | Default state of the search widget | + +### Events + +| Name | Type | Description | +| ---- | ---- | ----------- | +| searchChange | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when search widget value is changed. | +| submit | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when search widget is submited. | +| selectResult | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the result list is selected | +| reset | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the search widget is reseted | +| reset | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`` | Emitted when the search widget is reseted | \ No newline at end of file diff --git a/docs/docassets/images/search-text-input.png b/docs/docassets/images/search-text-input.png new file mode 100644 index 0000000000..d832bae196 Binary files /dev/null and b/docs/docassets/images/search-text-input.png differ diff --git a/lib/content-services/src/lib/search/components/search-control.component.html b/lib/content-services/src/lib/search/components/search-control.component.html index c72704c72d..587a5dbc29 100644 --- a/lib/content-services/src/lib/search/components/search-control.component.html +++ b/lib/content-services/src/lib/search/components/search-control.component.html @@ -1,81 +1,63 @@ - - - - search - - - - - - + + - - - - - - - - - - {{ item?.entry.name }} - - - - - {{item?.entry.createdByUser.displayName}} - - - - - - {{ 'SEARCH.RESULTS.NONE' | translate:{searchTerm: - searchTerm} }} - - - - - + + + + + + + + + + {{ item?.entry.name }} + + + + + {{item?.entry.createdByUser.displayName}} + + + + + + {{ 'SEARCH.RESULTS.NONE' | translate:{searchTerm: + searchTerm} }} + + + + + + diff --git a/lib/content-services/src/lib/search/components/search-control.component.scss b/lib/content-services/src/lib/search/components/search-control.component.scss index d2889894e6..9c00206bd1 100644 --- a/lib/content-services/src/lib/search/components/search-control.component.scss +++ b/lib/content-services/src/lib/search/components/search-control.component.scss @@ -7,38 +7,8 @@ $mat-menu-overlay-min-width: 112px !default; // 56 * 2 $mat-menu-overlay-max-width: 280px !default; // 56 * 5 - .adf-search-container { - overflow: hidden !important; - } - - .adf-search-button { - left: -13px; - } - - [dir='rtl'] .adf-search-button { - right: -13px; - } - - [dir='ltr'] .adf-search-button { - left: -13px; - } - .adf { - &-search-fixed-text { - line-height: normal; - } - - &-input-form-field-divider { - .mat-form-field-underline { - background-color: mat-color($primary, 50); - .mat-form-field-ripple { - background-color: mat-color($primary, 50); - } - } - font-size: 16px; - } - &-search-result-autocomplete { @include mat-overridable-elevation(2); @@ -74,8 +44,4 @@ } } } - - .adf-highlight { - color: mat-color($primary, 900); - } } diff --git a/lib/content-services/src/lib/search/components/search-control.component.spec.ts b/lib/content-services/src/lib/search/components/search-control.component.spec.ts index 80655d2ae0..68203ee202 100644 --- a/lib/content-services/src/lib/search/components/search-control.component.spec.ts +++ b/lib/content-services/src/lib/search/components/search-control.component.spec.ts @@ -16,19 +16,19 @@ */ import { Component, DebugElement, ViewChild } from '@angular/core'; -import { async, discardPeriodicTasks, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { AuthenticationService, SearchService, setupTestBed, CoreModule, - UserPreferencesService + UserPreferencesService, + SearchTextInputComponent } from '@alfresco/adf-core'; import { ThumbnailService } from '@alfresco/adf-core'; import { noResult, results } from '../../mock'; import { SearchControlComponent } from './search-control.component'; -import { SearchTriggerDirective } from './search-trigger.directive'; import { SearchComponent } from './search.component'; import { EmptySearchResultComponent } from './empty-search-result.component'; import { of } from 'rxjs'; @@ -51,6 +51,9 @@ export class SimpleSearchTestCustomEmptyComponent { @ViewChild(SearchControlComponent) searchComponent: SearchControlComponent; + @ViewChild(SearchTextInputComponent) + searchTextInputComponent: SearchTextInputComponent; + constructor() { } @@ -82,7 +85,6 @@ describe('SearchControlComponent', () => { declarations: [ SearchControlComponent, SearchComponent, - SearchTriggerDirective, EmptySearchResultComponent, SimpleSearchTestCustomEmptyComponent ], @@ -104,6 +106,7 @@ describe('SearchControlComponent', () => { element = fixture.nativeElement; searchServiceSpy = spyOn(searchService, 'search').and.returnValue(of('')); + fixture.detectChanges(); }); afterEach(() => { @@ -139,24 +142,8 @@ describe('SearchControlComponent', () => { fixture.detectChanges(); }); - it('should update FAYT search when user inputs a valid term', (done) => { - typeWordIntoSearchInput('customSearchTerm'); - spyOn(component, 'isSearchBarActive').and.returnValue(true); - searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); - - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(element.querySelector('#result_option_0')).not.toBeNull(); - expect(element.querySelector('#result_option_1')).not.toBeNull(); - expect(element.querySelector('#result_option_2')).not.toBeNull(); - done(); - }); - }); - it('should NOT update FAYT term when user inputs an empty string as search term ', (done) => { typeWordIntoSearchInput(''); - spyOn(component, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); fixture.detectChanges(); @@ -183,23 +170,6 @@ describe('SearchControlComponent', () => { }); }); - describe('expandable option false', () => { - - beforeEach(() => { - component.expandable = false; - fixture.detectChanges(); - }); - - it('search button should be hide', () => { - const searchButton: any = element.querySelector('#adf-search-button'); - expect(searchButton).toBe(null); - }); - - it('should not have animation', () => { - expect(component.subscriptAnimationState.value).toBe('no-animation'); - }); - }); - describe('component rendering', () => { it('should display a text input field by default', async(() => { @@ -215,18 +185,6 @@ describe('SearchControlComponent', () => { expect(attr).toBe('off'); })); - it('should display a search input field when specified', async(() => { - component.inputType = 'search'; - fixture.detectChanges(); - expect(element.querySelectorAll('input[type="search"]').length).toBe(1); - })); - - it('should set browser autocomplete to on when configured', async(() => { - component.autocomplete = true; - fixture.detectChanges(); - expect(element.querySelector('#adf-control-input').getAttribute('autocomplete')).toBe('on'); - })); - }); describe('autocomplete list', () => { @@ -241,8 +199,8 @@ describe('SearchControlComponent', () => { }); it('should make autocomplete list control visible when search box has focus and there is a search result', (done) => { - spyOn(component, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); + spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true); fixture.detectChanges(); typeWordIntoSearchInput('TEST'); @@ -255,8 +213,8 @@ describe('SearchControlComponent', () => { }); }); - it('should show autocomplete list noe results when search box has focus and there is search result with length 0', (done) => { - spyOn(component, 'isSearchBarActive').and.returnValue(true); + it('should show autocomplete list no results when search box has focus and there is search result with length 0', (done) => { + spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(noResult)); fixture.detectChanges(); @@ -271,8 +229,8 @@ describe('SearchControlComponent', () => { }); it('should hide autocomplete list results when the search box loses focus', (done) => { - spyOn(component, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); + spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true); fixture.detectChanges(); const inputDebugElement = debugElement.query(By.css('#adf-control-input')); @@ -292,8 +250,8 @@ describe('SearchControlComponent', () => { }); it('should keep autocomplete list control visible when user tabs into results', (done) => { - spyOn(component, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); + spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true); fixture.detectChanges(); const inputDebugElement = debugElement.query(By.css('#adf-control-input')); @@ -313,8 +271,8 @@ describe('SearchControlComponent', () => { }); it('should close the autocomplete when user press ESCAPE', (done) => { - spyOn(component, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); + spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true); fixture.detectChanges(); const inputDebugElement = debugElement.query(By.css('#adf-control-input')); @@ -337,7 +295,7 @@ describe('SearchControlComponent', () => { }); it('should close the autocomplete when user press ENTER on input', (done) => { - spyOn(component, 'isSearchBarActive').and.returnValue(true); + spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); fixture.detectChanges(); @@ -361,7 +319,7 @@ describe('SearchControlComponent', () => { }); it('should focus input element when autocomplete list is cancelled', (done) => { - spyOn(component, 'isSearchBarActive').and.returnValue(true); + searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); fixture.detectChanges(); @@ -393,108 +351,10 @@ describe('SearchControlComponent', () => { }); - describe('search button', () => { - - it('should NOT display a autocomplete list control when configured not to', fakeAsync(() => { - fixture.detectChanges(); - - tick(100); - - const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); - component.subscriptAnimationState.value = 'active'; - fixture.detectChanges(); - - tick(100); - - expect(component.subscriptAnimationState.value).toBe('active'); - - searchButton.triggerEventHandler('click', null); - fixture.detectChanges(); - - tick(100); - fixture.detectChanges(); - - tick(100); - - expect(component.subscriptAnimationState.value).toBe('inactive'); - discardPeriodicTasks(); - })); - - it('click on the search button should open the input box when is close', fakeAsync(() => { - fixture.detectChanges(); - - tick(100); - - const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); - searchButton.triggerEventHandler('click', null); - - tick(100); - fixture.detectChanges(); - - tick(100); - - expect(component.subscriptAnimationState.value).toBe('active'); - discardPeriodicTasks(); - })); - - it('Search button should not change the input state too often', fakeAsync(() => { - fixture.detectChanges(); - - tick(100); - - const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); - component.subscriptAnimationState.value = 'active'; - fixture.detectChanges(); - - tick(100); - - expect(component.subscriptAnimationState.value).toBe('active'); - searchButton.triggerEventHandler('click', null); - fixture.detectChanges(); - - tick(100); - - searchButton.triggerEventHandler('click', null); - fixture.detectChanges(); - - tick(100); - fixture.detectChanges(); - - tick(100); - - expect(component.subscriptAnimationState.value).toBe('inactive'); - discardPeriodicTasks(); - })); - - it('Search bar should close when user press ESC button', fakeAsync(() => { - fixture.detectChanges(); - - tick(100); - - const inputDebugElement = debugElement.query(By.css('#adf-control-input')); - component.subscriptAnimationState.value = 'active'; - fixture.detectChanges(); - - tick(100); - - expect(component.subscriptAnimationState.value).toBe('active'); - - inputDebugElement.triggerEventHandler('keyup.escape', {}); - - tick(100); - fixture.detectChanges(); - - tick(100); - - expect(component.subscriptAnimationState.value).toBe('inactive'); - discardPeriodicTasks(); - })); - }); - describe('option click', () => { it('should emit a option clicked event when item is clicked', (done) => { - spyOn(component, 'isSearchBarActive').and.returnValue(true); + spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); const clickDisposable = component.optionClicked.subscribe((item) => { expect(item.entry.id).toBe('123'); @@ -512,10 +372,10 @@ describe('SearchControlComponent', () => { }); it('should set deactivate the search after element is clicked', (done) => { - spyOn(component, 'isSearchBarActive').and.returnValue(true); + spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); const clickDisposable = component.optionClicked.subscribe(() => { - expect(component.subscriptAnimationState.value).toBe('inactive'); + expect(component.searchTextInput.subscriptAnimationState.value).toBe('inactive'); clickDisposable.unsubscribe(); done(); }); @@ -531,11 +391,11 @@ describe('SearchControlComponent', () => { }); it('should NOT reset the search term after element is clicked', (done) => { - spyOn(component, 'isSearchBarActive').and.returnValue(true); + spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results)))); const clickDisposable = component.optionClicked.subscribe(() => { - expect(component.searchTerm).not.toBeFalsy(); - expect(component.searchTerm).toBe('TEST'); + expect(component.searchTextInput.searchTerm).not.toBeFalsy(); + expect(component.searchTextInput.searchTerm).toBe('TEST'); clickDisposable.unsubscribe(); done(); }); @@ -557,13 +417,16 @@ describe('SearchControlComponent', () => { fixtureCustom = TestBed.createComponent(SimpleSearchTestCustomEmptyComponent); componentCustom = fixtureCustom.componentInstance; elementCustom = fixtureCustom.nativeElement; + fixture.detectChanges(); }); it('should display the custom no results when it is configured', (done) => { const noResultCustomMessage = 'BANDI IS NOTHING'; - spyOn(componentCustom.searchComponent, 'isSearchBarActive').and.returnValue(true); - componentCustom.setCustomMessageForNoResult(noResultCustomMessage); + spyOn(componentCustom.searchComponent, 'isLoggedIn').and.returnValue(true); + fixtureCustom.detectChanges(); + spyOn(componentCustom.searchComponent.searchTextInput, 'isSearchBarActive').and.returnValue(true); searchServiceSpy.and.returnValue(of(noResult)); + componentCustom.setCustomMessageForNoResult(noResultCustomMessage); fixtureCustom.detectChanges(); const inputDebugElement = fixtureCustom.debugElement.query(By.css('#adf-control-input')); @@ -589,82 +452,14 @@ describe('SearchControlComponent', () => { it('should have positive transform translation', () => { userPreferencesService.setWithoutStore('textOrientation', 'ltr'); fixture.detectChanges(); - expect(component.subscriptAnimationState.params.transform).toBe('translateX(82%)'); + expect(component.searchTextInput.subscriptAnimationState.params.transform).toBe('translateX(82%)'); }); it('should have negative transform translation ', () => { userPreferencesService.setWithoutStore('textOrientation', 'rtl'); fixture.detectChanges(); - expect(component.subscriptAnimationState.params.transform).toBe('translateX(-82%)'); + expect(component.searchTextInput.subscriptAnimationState.params.transform).toBe('translateX(-82%)'); }); }); - - describe('toggle animation', () => { - beforeEach(() => { - fixture.detectChanges(); - }); - - it('should have margin-left set when active and direction is ltr', fakeAsync(() => { - userPreferencesService.setWithoutStore('textOrientation', 'ltr'); - fixture.detectChanges(); - - const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); - - searchButton.triggerEventHandler('click', null); - tick(100); - fixture.detectChanges(); - tick(100); - - expect(component.subscriptAnimationState.params).toEqual({ 'margin-left': 13 }); - discardPeriodicTasks(); - })); - - it('should have positive transform translateX set when inactive and direction is ltr', fakeAsync(() => { - userPreferencesService.setWithoutStore('textOrientation', 'ltr'); - component.subscriptAnimationState.value = 'active'; - - fixture.detectChanges(); - const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); - - searchButton.triggerEventHandler('click', null); - tick(100); - fixture.detectChanges(); - tick(100); - - expect(component.subscriptAnimationState.params).toEqual({ 'transform': 'translateX(82%)' }); - discardPeriodicTasks(); - })); - - it('should have margin-right set when active and direction is rtl', fakeAsync(() => { - userPreferencesService.setWithoutStore('textOrientation', 'rtl'); - fixture.detectChanges(); - - const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); - - searchButton.triggerEventHandler('click', null); - tick(100); - fixture.detectChanges(); - tick(100); - - expect(component.subscriptAnimationState.params).toEqual({ 'margin-right': 13 }); - discardPeriodicTasks(); - })); - - it('should have negative transform translateX set when inactive and direction is rtl', fakeAsync(() => { - userPreferencesService.setWithoutStore('textOrientation', 'rtl'); - component.subscriptAnimationState.value = 'active'; - - fixture.detectChanges(); - const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); - - searchButton.triggerEventHandler('click', null); - tick(100); - fixture.detectChanges(); - tick(100); - - expect(component.subscriptAnimationState.params).toEqual({ 'transform': 'translateX(-82%)' }); - discardPeriodicTasks(); - })); - }); }); }); diff --git a/lib/content-services/src/lib/search/components/search-control.component.ts b/lib/content-services/src/lib/search/components/search-control.component.ts index 2c26c9e5be..a384ab0211 100644 --- a/lib/content-services/src/lib/search/components/search-control.component.ts +++ b/lib/content-services/src/lib/search/components/search-control.component.ts @@ -15,33 +15,23 @@ * limitations under the License. */ -import { AuthenticationService, ThumbnailService, UserPreferencesService } from '@alfresco/adf-core'; -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, - QueryList, ViewEncapsulation, ViewChild, ViewChildren, ElementRef, TemplateRef, ContentChild } from '@angular/core'; +import { AuthenticationService, ThumbnailService, SearchTextInputComponent } from '@alfresco/adf-core'; +import { Component, EventEmitter, Input, OnDestroy, Output, + QueryList, ViewEncapsulation, ViewChild, ViewChildren, TemplateRef, ContentChild } from '@angular/core'; import { NodeEntry } from '@alfresco/js-api'; -import { Observable, Subject } from 'rxjs'; +import { Subject } from 'rxjs'; import { SearchComponent } from './search.component'; -import { searchAnimation } from './animations'; import { MatListItem } from '@angular/material'; import { EmptySearchResultComponent } from './empty-search-result.component'; -import { debounceTime, filter, takeUntil } from 'rxjs/operators'; -import { Direction } from '@angular/cdk/bidi'; @Component({ selector: 'adf-search-control', templateUrl: './search-control.component.html', styleUrls: ['./search-control.component.scss'], - animations: [searchAnimation], encapsulation: ViewEncapsulation.None, host: { class: 'adf-search-control' } }) -export class SearchControlComponent implements OnInit, OnDestroy { - - /** Toggles whether to use an expanding search control. If false - * then a regular input is used. - */ - @Input() - expandable: boolean = true; +export class SearchControlComponent implements OnDestroy { /** Toggles highlighting of the search term in the results. */ @Input() @@ -51,13 +41,19 @@ export class SearchControlComponent implements OnInit, OnDestroy { @Input() inputType: string = 'text'; + /** Toggles "find-as-you-type" suggestions for possible matches. */ + @Input() + liveSearchEnabled: boolean = true; + /** Toggles auto-completion of the search input field. */ @Input() autocomplete: boolean = false; - /** Toggles "find-as-you-type" suggestions for possible matches. */ + /** Toggles whether to use an expanding search control. If false + * then a regular input is used. + */ @Input() - liveSearchEnabled: boolean = true; + expandable: boolean = true; /** Maximum number of results to show in the live search. */ @Input() @@ -81,87 +77,34 @@ export class SearchControlComponent implements OnInit, OnDestroy { @Output() optionClicked: EventEmitter = new EventEmitter(); + @ViewChild('searchTextInput') + searchTextInput: SearchTextInputComponent; + @ViewChild('search') searchAutocomplete: SearchComponent; - @ViewChild('searchInput') - searchInput: ElementRef; - @ViewChildren(MatListItem) private listResultElement: QueryList; @ContentChild(EmptySearchResultComponent) emptySearchTemplate: EmptySearchResultComponent; - searchTerm: string = ''; - subscriptAnimationState: any; + focusSubject = new Subject(); noSearchResultTemplate: TemplateRef = null; + searchTerm: string = ''; - private toggleSearch = new Subject(); - private focusSubject = new Subject(); private onDestroy$ = new Subject(); - private dir = 'ltr'; constructor( public authService: AuthenticationService, - private thumbnailService: ThumbnailService, - private userPreferencesService: UserPreferencesService - ) { - - this.toggleSearch - .pipe( - debounceTime(200), - takeUntil(this.onDestroy$) - ) - .subscribe(() => { - if (this.expandable) { - this.subscriptAnimationState = this.toggleAnimation(); - - if (this.subscriptAnimationState.value === 'inactive') { - this.searchTerm = ''; - this.searchAutocomplete.resetResults(); - if ( document.activeElement.id === this.searchInput.nativeElement.id) { - this.searchInput.nativeElement.blur(); - } - } - } - }); - } - - applySearchFocus(animationDoneEvent) { - if (animationDoneEvent.toState === 'active') { - this.searchInput.nativeElement.focus(); - } - } - - ngOnInit() { - this.userPreferencesService - .select('textOrientation') - .pipe(takeUntil(this.onDestroy$)) - .subscribe((direction: Direction) => { - this.dir = direction; - this.subscriptAnimationState = this.getAnimationState(); - }); - - this.subscriptAnimationState = this.getAnimationState(); - this.setupFocusEventHandlers(); - } + private thumbnailService: ThumbnailService + ) {} isNoSearchTemplatePresent(): boolean { return this.emptySearchTemplate ? true : false; } ngOnDestroy(): void { - if (this.focusSubject) { - this.focusSubject.complete(); - this.focusSubject = null; - } - - if (this.toggleSearch) { - this.toggleSearch.complete(); - this.toggleSearch = null; - } - this.onDestroy$.next(true); this.onDestroy$.complete(); } @@ -170,17 +113,9 @@ export class SearchControlComponent implements OnInit, OnDestroy { return this.authService.isEcmLoggedIn(); } - searchSubmit(event: any) { - this.submit.emit(event); - this.toggleSearchBar(); - } - - inputChange(event: any) { - this.searchChange.emit(event); - } - - getAutoComplete(): string { - return this.autocomplete ? 'on' : 'off'; + inputChange(value: string) { + this.searchTerm = value; + this.searchChange.emit(value); } getMimeTypeIcon(node: NodeEntry): string { @@ -200,20 +135,10 @@ export class SearchControlComponent implements OnInit, OnDestroy { return mimeType; } - isSearchBarActive() { - return this.subscriptAnimationState.value === 'active' && this.liveSearchEnabled; - } - - toggleSearchBar() { - if (this.toggleSearch) { - this.toggleSearch.next(); - } - } - elementClicked(item: any) { if (item.entry) { this.optionClicked.next(item); - this.toggleSearchBar(); + this.focusSubject.next(new FocusEvent('blur')); } } @@ -222,16 +147,13 @@ export class SearchControlComponent implements OnInit, OnDestroy { } onBlur($event): void { - this.focusSubject.next($event); - } - - activateToolbar() { - if (!this.isSearchBarActive()) { - this.toggleSearchBar(); + const nextElement: any = this.getNextElementSibling( $event.target); + if (!nextElement && !this.isListElement($event)) { + this.focusSubject.next($event); } } - selectFirstResult() { + onSelectFirstResult() { if ( this.listResultElement && this.listResultElement.length > 0) { const firstElement: MatListItem = this.listResultElement.first; firstElement._getHostElement().focus(); @@ -250,24 +172,18 @@ export class SearchControlComponent implements OnInit, OnDestroy { if (previousElement) { previousElement.focus(); } else { - this.searchInput.nativeElement.focus(); this.focusSubject.next(new FocusEvent('focus')); } } - private setupFocusEventHandlers() { - const focusEvents: Observable = this.focusSubject - .pipe( - debounceTime(50), - filter(($event: any) => { - return this.isSearchBarActive() && ($event.type === 'blur' || $event.type === 'focusout'); - }), - takeUntil(this.onDestroy$) - ); + onReset(status: boolean) { + if (status) { + this.searchAutocomplete.resetResults(); + } + } - focusEvents.subscribe(() => { - this.toggleSearchBar(); - }); + private isListElement($event: any): boolean { + return $event.relatedTarget && $event.relatedTarget.children[0].className === 'mat-list-item-content'; } private getNextElementSibling(node: Element): Element { @@ -277,28 +193,4 @@ export class SearchControlComponent implements OnInit, OnDestroy { private getPreviousElementSibling(node: Element): Element { return node.previousElementSibling; } - - private toggleAnimation() { - if (this.dir === 'ltr') { - return this.subscriptAnimationState.value === 'inactive' ? - { value: 'active', params: { 'margin-left': 13 } } : - { value: 'inactive', params: { 'transform': 'translateX(82%)' } }; - } else { - return this.subscriptAnimationState.value === 'inactive' ? - { value: 'active', params: { 'margin-right': 13 } } : - { value: 'inactive', params: { 'transform': 'translateX(-82%)' } }; - } - } - - private getAnimationState() { - if (this.dir === 'ltr') { - return this.expandable ? - { value: 'inactive', params: { 'transform': 'translateX(82%)' } } : - { value: 'no-animation' }; - } else { - return this.expandable ? - { value: 'inactive', params: { 'transform': 'translateX(-82%)' } } : - { value: 'no-animation' }; - } - } } diff --git a/lib/content-services/src/lib/search/components/search.component.ts b/lib/content-services/src/lib/search/components/search.component.ts index 66df77c6d9..e27add6095 100644 --- a/lib/content-services/src/lib/search/components/search.component.ts +++ b/lib/content-services/src/lib/search/components/search.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { SearchService } from '@alfresco/adf-core'; +import { SearchService, SearchComponentInterface } from '@alfresco/adf-core'; import { AfterContentInit, Component, @@ -45,7 +45,7 @@ import { debounceTime, takeUntil } from 'rxjs/operators'; 'class': 'adf-search' } }) -export class SearchComponent implements AfterContentInit, OnChanges, OnDestroy { +export class SearchComponent implements SearchComponentInterface, AfterContentInit, OnChanges, OnDestroy { @ViewChild('panel') panel: ElementRef; diff --git a/lib/content-services/src/lib/search/public-api.ts b/lib/content-services/src/lib/search/public-api.ts index 811b4c042f..727a8e9987 100644 --- a/lib/content-services/src/lib/search/public-api.ts +++ b/lib/content-services/src/lib/search/public-api.ts @@ -28,7 +28,6 @@ export { SearchRange } from './search-range.interface'; export * from './components/search.component'; export * from './components/search-control.component'; -export * from './components/search-trigger.directive'; export * from './components/empty-search-result.component'; export * from './components/search-filter/search-filter.component'; export * from './components/search-filter/search-filter.service'; diff --git a/lib/content-services/src/lib/search/search.module.ts b/lib/content-services/src/lib/search/search.module.ts index b61915b543..1a7f7e9ec0 100644 --- a/lib/content-services/src/lib/search/search.module.ts +++ b/lib/content-services/src/lib/search/search.module.ts @@ -22,8 +22,6 @@ import { MaterialModule } from '../material.module'; import { CoreModule } from '@alfresco/adf-core'; -import { SearchTriggerDirective } from './components/search-trigger.directive'; - import { SearchControlComponent } from './components/search-control.component'; import { SearchComponent } from './components/search.component'; import { EmptySearchResultComponent } from './components/empty-search-result.component'; @@ -41,7 +39,6 @@ import { SearchSortingPickerComponent } from './components/search-sorting-picker export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [ SearchComponent, SearchControlComponent, - SearchTriggerDirective, EmptySearchResultComponent, SearchFilterComponent, SearchChipListComponent diff --git a/lib/core/core.module.ts b/lib/core/core.module.ts index bf9b31548c..76c4e8846b 100644 --- a/lib/core/core.module.ts +++ b/lib/core/core.module.ts @@ -56,7 +56,7 @@ import { TranslateLoaderService } from './services/translate-loader.service'; import { ExtensionsModule } from '@alfresco/adf-extensions'; import { directionalityConfigFactory } from './services/directionality-config-factory'; import { DirectionalityConfigService } from './services/directionality-config.service'; -import { SearchCloudModule } from './search-cloud/search-cloud.module'; +import { SearchTextModule } from './search-text/search-text-input.module'; @NgModule({ imports: [ @@ -91,7 +91,7 @@ import { SearchCloudModule } from './search-cloud/search-cloud.module'; IconModule, SortingPickerModule, NotificationHistoryModule, - SearchCloudModule + SearchTextModule ], exports: [ AboutModule, @@ -125,7 +125,7 @@ import { SearchCloudModule } from './search-cloud/search-cloud.module'; SortingPickerModule, IconModule, NotificationHistoryModule, - SearchCloudModule + SearchTextModule ] }) export class CoreModule { diff --git a/lib/core/index.ts b/lib/core/index.ts index d2dba02de9..46c90a5416 100644 --- a/lib/core/index.ts +++ b/lib/core/index.ts @@ -42,7 +42,7 @@ export * from './clipboard/index'; export * from './dialogs/index'; export * from './icon/index'; export * from './notifications/index'; -export * from './search-cloud/index'; +export * from './search-text/index'; export * from './utils/index'; export * from './interface/index'; diff --git a/lib/core/interface/search-configuration.interface.ts b/lib/core/interface/search-configuration.interface.ts index 6cc0e97cdb..e237ad107e 100644 --- a/lib/core/interface/search-configuration.interface.ts +++ b/lib/core/interface/search-configuration.interface.ts @@ -15,7 +15,9 @@ * limitations under the License. */ -import { QueryBody } from '@alfresco/js-api'; +import { QueryBody, NodePaging } from '@alfresco/js-api'; +import { Subject } from 'rxjs'; +import { ElementRef } from '@angular/core'; export interface SearchConfigurationInterface { @@ -29,3 +31,17 @@ export interface SearchConfigurationInterface { generateQueryBody(searchTerm: string, maxResults: number, skipCount: number): QueryBody; } + +export interface SearchComponentInterface { + + panel: ElementRef; + showPanel: boolean; + results: NodePaging; + isOpen: boolean; + keyPressedStream: Subject; + displayWith: ((value: any) => string) | null; + + resetResults(): void; + hidePanel(): void; + setVisibility(): void; +} diff --git a/lib/core/models/public-api.ts b/lib/core/models/public-api.ts index 3343b8a2b0..5f402d7933 100644 --- a/lib/core/models/public-api.ts +++ b/lib/core/models/public-api.ts @@ -33,3 +33,4 @@ export * from './identity-group.model'; export * from './identity-user.model'; export * from './identity-role.model'; export * from './identity-group.model'; +export * from './search-text-input.model'; diff --git a/lib/core/models/search-cloud.model.ts b/lib/core/models/search-cloud.model.ts deleted file mode 100644 index 8dfc10afb0..0000000000 --- a/lib/core/models/search-cloud.model.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*! - * @license - * Copyright 2019 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - import { SearchTextCloudComponent } from '../search-cloud/components/search-text-cloud/search-text-cloud.component'; - - export interface SearchCloudProperties { - value?: string; - placeholder?: string; - debounceTime?: number; - expandable?: boolean; - } - - export enum SearchCloudTypesEnum { - text = 'text' - } - - export const SEARCH_CLOUD_TYPES = { - text: SearchTextCloudComponent - }; - - export interface SearchCloudWidget { - properties: SearchCloudProperties; - onChangedHandler(event: any); - } diff --git a/lib/core/services/search-cloud.service.ts b/lib/core/models/search-text-input.model.ts similarity index 61% rename from lib/core/services/search-cloud.service.ts rename to lib/core/models/search-text-input.model.ts index d2efbe4fe3..10b4b3436e 100644 --- a/lib/core/services/search-cloud.service.ts +++ b/lib/core/models/search-text-input.model.ts @@ -15,12 +15,22 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import { Subject } from 'rxjs'; - -@Injectable({ - providedIn: 'root' -}) -export class SearchCloudService { - value = new Subject(); +export enum SearchTextStateEnum { + expanded = 'expanded', + collapsed = 'collapsed' +} + +export interface SearchAnimationState { + value: string; + params?: any; +} + +export interface SearchAnimationControl { + active: SearchAnimationState; + inactive: SearchAnimationState; +} + +export interface SearchAnimationDirection { + ltr: SearchAnimationControl; + rtl: SearchAnimationControl; } diff --git a/lib/core/search-cloud/components/search-text-cloud/search-text-cloud.component.html b/lib/core/search-cloud/components/search-text-cloud/search-text-cloud.component.html deleted file mode 100644 index fb58038305..0000000000 --- a/lib/core/search-cloud/components/search-text-cloud/search-text-cloud.component.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - 0" (click)="clear()">close - - - search - \ No newline at end of file diff --git a/lib/core/search-cloud/components/search-text-cloud/search-text-cloud.component.spec.ts b/lib/core/search-cloud/components/search-text-cloud/search-text-cloud.component.spec.ts deleted file mode 100644 index 6c598b9e06..0000000000 --- a/lib/core/search-cloud/components/search-text-cloud/search-text-cloud.component.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*! - * @license - * Copyright 2019 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { SearchTextCloudComponent } from './search-text-cloud.component'; -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { setupTestBed } from 'core'; -import { CoreTestingModule } from '../../../testing/core.testing.module'; -import { SearchCloudService } from '../../../services/search-cloud.service'; - -describe('SearchTextCloudComponent', () => { - - let fixture: ComponentFixture; - let service: SearchCloudService; - - setupTestBed({ - imports: [CoreTestingModule] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(SearchTextCloudComponent); - service = TestBed.get(SearchCloudService); - }); - - afterEach(() => { - fixture.destroy(); - }); - - it('should update search service value when is field is changed', async(() => { - fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - const searchInput = fixture.nativeElement.querySelector('.adf-search-text-cloud input'); - searchInput.value = 'mock-search-text'; - searchInput.dispatchEvent(new Event('input')); - fixture.detectChanges(); - }); - - service.value.subscribe( value => { - expect(value).toBe('mock-search-text'); - }); - })); - -}); diff --git a/lib/core/search-cloud/components/search-text-cloud/search-text-cloud.component.ts b/lib/core/search-cloud/components/search-text-cloud/search-text-cloud.component.ts deleted file mode 100644 index 9599f50488..0000000000 --- a/lib/core/search-cloud/components/search-text-cloud/search-text-cloud.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -/*! - * @license - * Copyright 2019 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ViewEncapsulation, Component, ViewChild, ElementRef, Renderer2, OnInit } from '@angular/core'; -import { Subject } from 'rxjs'; -import { SearchCloudService } from '../../../services/search-cloud.service'; -import { SearchCloudProperties, SearchCloudWidget } from '../../../models/search-cloud.model'; - -@Component({ - selector: 'adf-search-text-cloud', - templateUrl: './search-text-cloud.component.html', - encapsulation: ViewEncapsulation.None -}) -export class SearchTextCloudComponent implements OnInit, SearchCloudWidget { - - @ViewChild('searchContainer') - searchInput: ElementRef; - - properties: SearchCloudProperties = {}; - onDestroy$: Subject = new Subject(); - - expandedClass = 'app-field-expanded'; - - constructor( - private searchCloudService: SearchCloudService, - private renderer: Renderer2) {} - - ngOnInit() { - if (!this.isExpandable()) { - this.renderer.addClass(this.searchInput.nativeElement, this.expandedClass); - } - } - - onChangedHandler(event) { - this.searchCloudService.value.next(event.target.value); - } - - toggle() { - if (!this.isExpandable()) { return; } - - if (this.searchInput.nativeElement && this.searchInput.nativeElement.classList.contains(this.expandedClass)) { - this.renderer.removeClass(this.searchInput.nativeElement, this.expandedClass); - } else { - this.renderer.addClass(this.searchInput.nativeElement, this.expandedClass); - } - } - - private isExpandable() { - return this.properties && this.properties.expandable; - } - - clear() { - this.properties.value = ''; - } -} diff --git a/lib/core/search-cloud/search-cloud.component.scss b/lib/core/search-cloud/search-cloud.component.scss deleted file mode 100644 index 72cca2c67d..0000000000 --- a/lib/core/search-cloud/search-cloud.component.scss +++ /dev/null @@ -1,33 +0,0 @@ -.adf-search-cloud { - - &-wrapper { - position: relative; - margin: 10px; - width: 260px; - - &.app-field-expanded { - mat-form-field { - display: block; - width: 220px; - } - } - } - - &-icon { - fill: currentColor; - width: 24px; - height: 24px; - line-height: 65px; - float: right; - } - - mat-form-field { - float: right; - width:0; - margin-left: 10px; - - -webkit-transition: all 0.5s ease; - -moz-transition: all 0.5s ease; - transition: all 0.5s ease; - } -} diff --git a/lib/core/search-cloud/search-cloud.component.spec.ts b/lib/core/search-cloud/search-cloud.component.spec.ts deleted file mode 100644 index e2c172e4b9..0000000000 --- a/lib/core/search-cloud/search-cloud.component.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*! - * @license - * Copyright 2019 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; -import { setupTestBed } from 'core'; -import { CoreTestingModule } from '../testing/core.testing.module'; -import { SearchCloudComponent } from './search-cloud.component'; -import { SearchCloudTypesEnum } from '../models/search-cloud.model'; - -describe('SearchCloudComponent', () => { - - let fixture: ComponentFixture; - let component: SearchCloudComponent; - - setupTestBed({ - imports: [CoreTestingModule] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(SearchCloudComponent); - component = fixture.componentInstance; - }); - - afterEach(() => { - fixture.destroy(); - }); - - it('should emit search text when is field is changed', async(() => { - spyOn(component.change, 'emit'); - component.type = SearchCloudTypesEnum.text; - component.ngOnInit(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - const searchInput = fixture.nativeElement.querySelector('.adf-search-text-cloud input'); - searchInput.value = 'mock-search-text'; - searchInput.dispatchEvent(new Event('input')); - fixture.detectChanges(); - }); - - component.change.subscribe( emitValue => { - expect(emitValue).toBe('mock-search-text'); - }); - })); - -}); diff --git a/lib/core/search-cloud/search-cloud.component.ts b/lib/core/search-cloud/search-cloud.component.ts deleted file mode 100644 index 08a9e11c4e..0000000000 --- a/lib/core/search-cloud/search-cloud.component.ts +++ /dev/null @@ -1,95 +0,0 @@ -/*! - * @license - * Copyright 2019 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ViewEncapsulation, Component, OnInit, ComponentRef, ViewChild, ViewContainerRef, Input, ComponentFactoryResolver, OnDestroy, EventEmitter, Output } from '@angular/core'; -import { SearchCloudService } from '../services/search-cloud.service'; -import { Subject } from 'rxjs'; -import { takeUntil, debounceTime } from 'rxjs/operators'; -import { SearchCloudTypesEnum, SEARCH_CLOUD_TYPES, SearchCloudProperties } from '../models/search-cloud.model'; - -@Component({ - selector: 'adf-search-cloud', - template: '', - styleUrls: ['./search-cloud.component.scss'], - encapsulation: ViewEncapsulation.None, - host: { - 'class': 'adf-search-cloud' - } -}) -export class SearchCloudComponent implements OnInit, OnDestroy { - - @Input() value: string = ''; - - @Input() debounceTime: number = 500; - - @Input() placeholder: string; - - @Input() expandable: boolean = false; - - @Input() type: SearchCloudTypesEnum; - - @ViewChild('container', { read: ViewContainerRef }) - container: ViewContainerRef; - - @Output() change: EventEmitter = new EventEmitter(); - - private componentRef: ComponentRef; - onDestroy$: Subject = new Subject(); - - constructor ( - private searchCloudService: SearchCloudService, - private componentFactoryResolver: ComponentFactoryResolver) {} - - ngOnInit() { - const componentType = SEARCH_CLOUD_TYPES[this.type]; - if (componentType) { - const factory = this.componentFactoryResolver.resolveComponentFactory(componentType); - if (factory) { - this.componentRef = this.container.createComponent(factory, 0); - this.setupWidget(); - } - } - - this.searchCloudService.value - .pipe( - debounceTime(this.debounceTime), - takeUntil(this.onDestroy$) - ) - .subscribe( (value: string) => { - this.change.emit(value); - }); - } - - setupWidget() { - if (this.componentRef && this.componentRef.instance) { - const properties: SearchCloudProperties = { - placeholder: this.placeholder, - debounceTime: this.debounceTime, - expandable: this.expandable, - value: this.value - }; - this.componentRef.instance.properties = properties; - } - } - - ngOnDestroy() { - if (this.componentRef) { - this.componentRef.destroy(); - this.componentRef = null; - } - } -} diff --git a/lib/content-services/src/lib/search/components/animations.ts b/lib/core/search-text/animations.ts similarity index 100% rename from lib/content-services/src/lib/search/components/animations.ts rename to lib/core/search-text/animations.ts diff --git a/lib/core/search-cloud/index.ts b/lib/core/search-text/index.ts similarity index 100% rename from lib/core/search-cloud/index.ts rename to lib/core/search-text/index.ts diff --git a/lib/core/search-cloud/public-api.ts b/lib/core/search-text/public-api.ts similarity index 86% rename from lib/core/search-cloud/public-api.ts rename to lib/core/search-text/public-api.ts index 547f32411a..12ad5e3cb5 100644 --- a/lib/core/search-cloud/public-api.ts +++ b/lib/core/search-text/public-api.ts @@ -15,5 +15,5 @@ * limitations under the License. */ -export * from './search-cloud.component'; -export * from './search-cloud.module'; + export * from './search-text-input.component'; + export * from './search-text-input.module'; diff --git a/lib/core/search-text/search-text-input.component.html b/lib/core/search-text/search-text-input.component.html new file mode 100644 index 0000000000..fcb89d7e24 --- /dev/null +++ b/lib/core/search-text/search-text-input.component.html @@ -0,0 +1,30 @@ + + + + search + + + + + + \ No newline at end of file diff --git a/lib/core/search-text/search-text-input.component.scss b/lib/core/search-text/search-text-input.component.scss new file mode 100644 index 0000000000..ae89070c3a --- /dev/null +++ b/lib/core/search-text/search-text-input.component.scss @@ -0,0 +1,46 @@ +@mixin adf-search-text-input-theme($theme) { + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $mat-menu-border-radius: 2px !default; + $mat-menu-overlay-min-width: 112px !default; // 56 * 2 + $mat-menu-overlay-max-width: 280px !default; // 56 * 5 + + .adf-search-container { + overflow: hidden !important; + } + + .adf-search-button { + left: -13px; + } + + [dir='rtl'] .adf-search-button { + right: -13px; + } + + [dir='ltr'] .adf-search-button { + left: -13px; + } + + .adf { + + &-search-fixed-text { + line-height: normal; + } + + &-input-form-field-divider { + .mat-form-field-underline { + background-color: mat-color($primary, 50); + .mat-form-field-ripple { + background-color: mat-color($primary, 50); + } + } + font-size: 16px; + } + } + + .adf-highlight { + color: mat-color($primary, 900); + } +} diff --git a/lib/core/search-text/search-text-input.component.spec.ts b/lib/core/search-text/search-text-input.component.spec.ts new file mode 100644 index 0000000000..0d46bbe486 --- /dev/null +++ b/lib/core/search-text/search-text-input.component.spec.ts @@ -0,0 +1,248 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed, discardPeriodicTasks, fakeAsync, tick, async } from '@angular/core/testing'; +import { setupTestBed, UserPreferencesService } from 'core'; +import { CoreTestingModule } from '../testing/core.testing.module'; +import { SearchTextInputComponent } from './search-text-input.component'; +import { DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { Subject } from 'rxjs'; + +describe('SearchTextInputComponent', () => { + + let fixture: ComponentFixture; + let component: SearchTextInputComponent; + let debugElement: DebugElement; + let element: HTMLElement; + let userPreferencesService: UserPreferencesService; + + setupTestBed({ + imports: [CoreTestingModule], + providers: [ UserPreferencesService ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchTextInputComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + element = fixture.nativeElement; + userPreferencesService = TestBed.get(UserPreferencesService); + component.focusListener = new Subject(); + }); + + afterEach(() => { + fixture.destroy(); + }); + + describe('component rendering', () => { + + it('should display a search input field when specified', async(() => { + component.inputType = 'search'; + fixture.detectChanges(); + expect(element.querySelectorAll('input[type="search"]').length).toBe(1); + })); + }); + + describe('expandable option false', () => { + + beforeEach(() => { + component.expandable = false; + fixture.detectChanges(); + }); + + it('search button should be hide', () => { + const searchButton: any = element.querySelector('#adf-search-button'); + expect(searchButton).toBe(null); + }); + + it('should not have animation', () => { + expect(component.subscriptAnimationState.value).toBe('no-animation'); + }); + }); + + describe('search button', () => { + + it('should NOT display a autocomplete list control when configured not to', fakeAsync(() => { + fixture.detectChanges(); + + tick(100); + + const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); + component.subscriptAnimationState.value = 'active'; + fixture.detectChanges(); + + tick(100); + + expect(component.subscriptAnimationState.value).toBe('active'); + + searchButton.triggerEventHandler('click', null); + fixture.detectChanges(); + tick(100); + fixture.detectChanges(); + + tick(100); + + expect(component.subscriptAnimationState.value).toBe('inactive'); + discardPeriodicTasks(); + })); + + it('click on the search button should open the input box when is close', fakeAsync(() => { + fixture.detectChanges(); + + tick(100); + + const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); + searchButton.triggerEventHandler('click', null); + + tick(100); + fixture.detectChanges(); + + tick(100); + + expect(component.subscriptAnimationState.value).toBe('active'); + discardPeriodicTasks(); + })); + + it('Search button should not change the input state too often', fakeAsync(() => { + fixture.detectChanges(); + + tick(100); + + const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); + component.subscriptAnimationState.value = 'active'; + fixture.detectChanges(); + + tick(100); + + expect(component.subscriptAnimationState.value).toBe('active'); + searchButton.triggerEventHandler('click', null); + fixture.detectChanges(); + + tick(100); + + searchButton.triggerEventHandler('click', null); + fixture.detectChanges(); + + tick(100); + fixture.detectChanges(); + + tick(100); + + expect(component.subscriptAnimationState.value).toBe('inactive'); + discardPeriodicTasks(); + })); + + it('Search bar should close when user press ESC button', fakeAsync(() => { + fixture.detectChanges(); + + tick(100); + + const inputDebugElement = debugElement.query(By.css('#adf-control-input')); + component.subscriptAnimationState.value = 'active'; + fixture.detectChanges(); + + tick(100); + + expect(component.subscriptAnimationState.value).toBe('active'); + + inputDebugElement.triggerEventHandler('keyup.escape', {}); + + tick(100); + fixture.detectChanges(); + + tick(100); + + expect(component.subscriptAnimationState.value).toBe('inactive'); + discardPeriodicTasks(); + })); + }); + + describe('toggle animation', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should have margin-left set when active and direction is ltr', fakeAsync(() => { + userPreferencesService.setWithoutStore('textOrientation', 'ltr'); + fixture.detectChanges(); + + const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); + + searchButton.triggerEventHandler('click', null); + tick(100); + fixture.detectChanges(); + tick(100); + + expect(component.subscriptAnimationState.params).toEqual({ 'margin-left': 13 }); + discardPeriodicTasks(); + })); + + it('should have positive transform translateX set when inactive and direction is ltr', fakeAsync(() => { + userPreferencesService.setWithoutStore('textOrientation', 'ltr'); + component.subscriptAnimationState.value = 'active'; + + fixture.detectChanges(); + const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); + + searchButton.triggerEventHandler('click', null); + tick(100); + fixture.detectChanges(); + tick(100); + + expect(component.subscriptAnimationState.params).toEqual({ 'transform': 'translateX(82%)' }); + discardPeriodicTasks(); + })); + + it('should have margin-right set when active and direction is rtl', fakeAsync(() => { + userPreferencesService.setWithoutStore('textOrientation', 'rtl'); + fixture.detectChanges(); + + const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); + + searchButton.triggerEventHandler('click', null); + tick(100); + fixture.detectChanges(); + tick(100); + + expect(component.subscriptAnimationState.params).toEqual({ 'margin-right': 13 }); + discardPeriodicTasks(); + })); + + it('should have negative transform translateX set when inactive and direction is rtl', fakeAsync(() => { + userPreferencesService.setWithoutStore('textOrientation', 'rtl'); + component.subscriptAnimationState.value = 'active'; + + fixture.detectChanges(); + const searchButton: DebugElement = debugElement.query(By.css('#adf-search-button')); + + searchButton.triggerEventHandler('click', null); + tick(100); + fixture.detectChanges(); + tick(100); + + expect(component.subscriptAnimationState.params).toEqual({ 'transform': 'translateX(-82%)' }); + discardPeriodicTasks(); + })); + + it('should set browser autocomplete to on when configured', async(() => { + component.autocomplete = true; + fixture.detectChanges(); + expect(element.querySelector('#adf-control-input').getAttribute('autocomplete')).toBe('on'); + })); + }); +}); diff --git a/lib/core/search-text/search-text-input.component.ts b/lib/core/search-text/search-text-input.component.ts new file mode 100644 index 0000000000..2c996fcbf7 --- /dev/null +++ b/lib/core/search-text/search-text-input.component.ts @@ -0,0 +1,270 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ViewEncapsulation, Component, Input, OnDestroy, ViewChild, ElementRef, Output, EventEmitter, OnInit } from '@angular/core'; +import { Subject, Observable, Subscription } from 'rxjs'; +import { debounceTime, takeUntil, filter } from 'rxjs/operators'; +import { Direction } from '@angular/cdk/bidi'; +import { searchAnimation } from './animations'; +import { UserPreferencesService } from '../services/user-preferences.service'; +import { SearchTextStateEnum, SearchAnimationState, SearchAnimationDirection } from '../models/search-text-input.model'; + +@Component({ + selector: 'adf-search-text-input', + templateUrl: './search-text-input.component.html', + styleUrls: ['./search-text-input.component.scss'], + animations: [searchAnimation], + encapsulation: ViewEncapsulation.None, + host: { + 'class': 'adf-search-text-input' + } +}) +export class SearchTextInputComponent implements OnInit, OnDestroy { + + /** Toggles auto-completion of the search input field. */ + @Input() + autocomplete: boolean = false; + + /** Toggles whether to use an expanding search control. If false + * then a regular input is used. + */ + @Input() + expandable: boolean = true; + + /** Type of the input field to render, e.g. "search" or "text" (default). */ + @Input() + inputType: string = 'text'; + + /** Toggles "find-as-you-type" suggestions for possible matches. */ + @Input() + liveSearchEnabled: boolean = true; + + @Input() + searchAutocomplete: any = false; + + @Input() + searchTerm: string = ''; + + @Input() + debounceTime: number = 0; + + @Input() + focusListener: Observable; + + @Input() + defaultState: SearchTextStateEnum = SearchTextStateEnum.collapsed; + + /** 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 it is truncated to an empty + * string. + */ + @Output() + searchChange: EventEmitter = new EventEmitter(); + + /** Emitted when the search is submitted by pressing the ENTER key. + * The search term is provided as the value of the event. + */ + @Output() + submit: EventEmitter = new EventEmitter(); + + @Output() + selectResult: EventEmitter = new EventEmitter(); + + @Output() + reset: EventEmitter = new EventEmitter(); + + @ViewChild('searchInput') + searchInput: ElementRef; + + subscriptAnimationState: any; + + animationStates: SearchAnimationDirection = { + ltr : { + active: { value: 'active', params: { 'margin-left': 13 } }, + inactive: { value: 'inactive', params: { 'transform': 'translateX(82%)' } } + }, + rtl: { + active: { value: 'active', params: { 'margin-right': 13 } }, + inactive: { value: 'inactive', params: { 'transform': 'translateX(-82%)' } } + } + }; + + private dir = 'ltr'; + private onDestroy$ = new Subject(); + private toggleSearch = new Subject(); + private focusSubscription: Subscription; + private valueChange = new Subject(); + + constructor ( + private userPreferencesService: UserPreferencesService + ) { + this.toggleSearch + .pipe( + debounceTime(200), + takeUntil(this.onDestroy$) + ) + .subscribe(() => { + if (this.expandable) { + this.subscriptAnimationState = this.toggleAnimation(); + if (this.subscriptAnimationState.value === 'inactive') { + this.searchTerm = ''; + this.reset.emit(true); + if ( document.activeElement.id === this.searchInput.nativeElement.id) { + this.searchInput.nativeElement.blur(); + } + } + } + }); + } + + ngOnInit() { + this.userPreferencesService + .select('textOrientation') + .pipe(takeUntil(this.onDestroy$)) + .subscribe((direction: Direction) => { + this.dir = direction; + this.subscriptAnimationState = this.getDefaultState(this.dir); + }); + + this.subscriptAnimationState = this.getDefaultState(this.dir); + this.setValueChangeHandler(); + this.setupFocusEventHandlers(); + } + + applySearchFocus(animationDoneEvent) { + if (animationDoneEvent.toState === 'active') { + this.searchInput.nativeElement.focus(); + } + } + + getAutoComplete(): string { + return this.autocomplete ? 'on' : 'off'; + } + + private toggleAnimation() { + if (this.dir === 'ltr') { + return this.subscriptAnimationState.value === 'inactive' ? + { value: 'active', params: { 'margin-left': 13 } } : + { value: 'inactive', params: { 'transform': 'translateX(82%)' } }; + } else { + return this.subscriptAnimationState.value === 'inactive' ? + { value: 'active', params: { 'margin-right': 13 } } : + { value: 'inactive', params: { 'transform': 'translateX(-82%)' } }; + } + } + + private getDefaultState(dir: string): SearchAnimationState { + if (this.dir) { + return this.getAnimationState(dir); + } + return this.animationStates.ltr.inactive; + } + + private getAnimationState(dir: string): SearchAnimationState { + if ( this.expandable && this.defaultState === SearchTextStateEnum.expanded ) { + return this.animationStates[dir].active; + } else if ( this.expandable ) { + return this.animationStates[dir].inactive; + } else { + return { value: 'no-animation' }; + } + } + + private setupFocusEventHandlers() { + if ( this.focusListener ) { + const focusEvents: Observable = this.focusListener + .pipe( + debounceTime(50), + filter(($event: any) => { + return this.isSearchBarActive() && ($event.type === 'blur' || $event.type === 'focusout' || $event.type === 'focus'); + }), + takeUntil(this.onDestroy$) + ); + + this.focusSubscription = focusEvents.subscribe( (event: FocusEvent) => { + if ( event.type === 'focus') { + this.searchInput.nativeElement.focus(); + } else { + this.toggleSearchBar(); + } + }); + } + } + + private setValueChangeHandler() { + this.valueChange.pipe( + debounceTime(this.debounceTime), + takeUntil(this.onDestroy$) + ).subscribe( (value: string) => { + this.searchChange.emit(value); + }); + } + + selectFirstResult($event) { + this.selectResult.emit($event); + } + + onBlur($event) { + if (!$event.relatedTarget && this.defaultState === SearchTextStateEnum.collapsed) { + this.searchTerm = ''; + this.subscriptAnimationState = this.animationStates[this.dir].inactive; + } + } + + inputChange($event: any) { + this.valueChange.next($event); + } + + toggleSearchBar() { + if (this.toggleSearch) { + this.toggleSearch.next(); + } + } + + searchSubmit(event: any) { + this.submit.emit(event); + this.toggleSearchBar(); + } + + activateToolbar(): boolean { + if (!this.isSearchBarActive()) { + this.toggleSearchBar(); + } + return false; + } + + isSearchBarActive(): boolean { + return this.subscriptAnimationState.value === 'active' && this.liveSearchEnabled; + } + + ngOnDestroy() { + if (this.toggleSearch) { + this.toggleSearch.complete(); + this.toggleSearch = null; + } + + if (this.focusSubscription) { + this.focusSubscription.unsubscribe(); + this.focusSubscription = null; + this.focusListener = null; + } + + this.onDestroy$.next(true); + this.onDestroy$.complete(); + } +} diff --git a/lib/core/search-cloud/search-cloud.module.ts b/lib/core/search-text/search-text-input.module.ts similarity index 69% rename from lib/core/search-cloud/search-cloud.module.ts rename to lib/core/search-text/search-text-input.module.ts index 3d3f7b6f74..4d17fd5094 100644 --- a/lib/core/search-cloud/search-cloud.module.ts +++ b/lib/core/search-text/search-text-input.module.ts @@ -16,23 +16,27 @@ */ import { NgModule } from '@angular/core'; -import { SearchCloudComponent } from './search-cloud.component'; -import { SearchTextCloudComponent } from './components/search-text-cloud/search-text-cloud.component'; import { CommonModule } from '@angular/common'; import { MaterialModule } from '../material.module'; import { FormsModule } from '@angular/forms'; +import { SearchTextInputComponent } from './search-text-input.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { SearchTriggerDirective } from './search-trigger.directive'; @NgModule({ declarations: [ - SearchCloudComponent, - SearchTextCloudComponent + SearchTextInputComponent, + SearchTriggerDirective ], imports: [ CommonModule, + TranslateModule.forChild(), MaterialModule, FormsModule ], - exports: [ SearchCloudComponent], - entryComponents: [ SearchTextCloudComponent ] + exports: [ + SearchTextInputComponent, + SearchTriggerDirective + ] }) -export class SearchCloudModule {} +export class SearchTextModule {} diff --git a/lib/content-services/src/lib/search/components/search-trigger.directive.ts b/lib/core/search-text/search-trigger.directive.ts similarity index 93% rename from lib/content-services/src/lib/search/components/search-trigger.directive.ts rename to lib/core/search-text/search-trigger.directive.ts index f5dafb6df9..0baa0fc40b 100644 --- a/lib/content-services/src/lib/search/components/search-trigger.directive.ts +++ b/lib/core/search-text/search-trigger.directive.ts @@ -32,8 +32,8 @@ import { import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { DOCUMENT } from '@angular/common'; import { Observable, Subject, Subscription, merge, of, fromEvent } from 'rxjs'; -import { SearchComponent } from './search.component'; import { filter, switchMap, takeUntil } from 'rxjs/operators'; +import { SearchComponentInterface } from '../../core/interface/search-configuration.interface'; export const SEARCH_AUTOCOMPLETE_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, @@ -62,7 +62,7 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy { private onDestroy$: Subject = new Subject(); @Input('searchAutocomplete') - searchPanel: SearchComponent; + searchPanel: SearchComponentInterface; @Input() autocomplete: string = 'off'; @@ -161,13 +161,13 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy { } handleInput(event: KeyboardEvent): void { - if (document.activeElement === event.target) { + if (document.activeElement === event.target ) { const inputValue: string = (event.target as HTMLInputElement).value; this.onChange(inputValue); - if (inputValue) { + if (inputValue && this.searchPanel) { this.searchPanel.keyPressedStream.next(inputValue); this.openPanel(); - } else { + } else if (this.searchPanel) { this.searchPanel.resetResults(); this.closePanel(); } @@ -176,7 +176,7 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy { private isPanelOptionClicked(event: MouseEvent) { let isPanelOption: boolean = false; - if ( event ) { + if ( event && this.searchPanel ) { const clickTarget = event.target as HTMLElement; isPanelOption = !this.isNoResultOption() && !!this.searchPanel.panel && @@ -185,8 +185,8 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy { return isPanelOption; } - private isNoResultOption() { - return this.searchPanel.results.list ? this.searchPanel.results.list.entries.length === 0 : true; + private isNoResultOption(): boolean { + return this.searchPanel && this.searchPanel.results.list ? this.searchPanel.results.list.entries.length === 0 : true; } private subscribeToClosingActions(): Subscription { diff --git a/lib/core/styles/_index.scss b/lib/core/styles/_index.scss index ec01d8a97b..9e39f96a34 100644 --- a/lib/core/styles/_index.scss +++ b/lib/core/styles/_index.scss @@ -33,6 +33,7 @@ @import '../login/components/login-dialog.component'; @import '../login/components/login-dialog-panel.component'; @import '../../core/clipboard/clipboard.component'; +@import '../../core/search-text/search-text-input.component'; @import './snackbar'; @mixin adf-core-theme($theme) { @@ -71,4 +72,5 @@ @include adf-clipboard-theme($theme); @include adf-snackbar-theme($theme); @include mat-expansion-panel-theme--fix($theme); + @include adf-search-text-input-theme($theme); }
{{item?.entry.createdByUser.displayName}}
{{ 'SEARCH.RESULTS.NONE' | translate:{searchTerm: - searchTerm} }}
{{ 'SEARCH.RESULTS.NONE' | translate:{searchTerm: + searchTerm} }}