mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[AAE-534] Search - extract search text input from SearchControlComponent (#5213)
* [AAE-534] - add search cloud component * change doc file * more changes on doc file * remove empty scss file * add animation and more customizations * add preselect value property * fix unit tests * [AAE-534] Search - extract search text input from SearchControlComponent * rename component and fix build * fix import scss and lint * change in doc files * PR changes * more changes * add return type * fix unit test
This commit is contained in:
committed by
Eugenio Romano
parent
c11ce016fa
commit
f20b78a2c5
@@ -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
|
|
||||||
<adf-search-cloud>
|
|
||||||
[type]="'text'"
|
|
||||||
[placeholder]="'placeholder'"
|
|
||||||
[debounceTime]="200"
|
|
||||||
[expandable]='false'
|
|
||||||
(change)="onSearchValueChanged($event)"
|
|
||||||
</adf-search-cloud>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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)`<string>` | Emitted when search widget value is changed. |
|
|
50
docs/core/components/search-text-input.component.md
Normal file
50
docs/core/components/search-text-input.component.md
Normal file
@@ -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
|
||||||
|
<adf-search-text-input
|
||||||
|
[expandable]="true"
|
||||||
|
[autocomplete]="autocomplete"
|
||||||
|
[liveSearchEnabled]="liveSearchEnabled"
|
||||||
|
(searchChange)="onSearchChanged($event)">
|
||||||
|
</adf-search-text-input>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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)`<string>` | Emitted when search widget value is changed. |
|
||||||
|
| submit | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when search widget is submited. |
|
||||||
|
| selectResult | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when the result list is selected |
|
||||||
|
| reset | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when the search widget is reseted |
|
||||||
|
| reset | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when the search widget is reseted |
|
BIN
docs/docassets/images/search-text-input.png
Normal file
BIN
docs/docassets/images/search-text-input.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.0 KiB |
@@ -1,46 +1,27 @@
|
|||||||
<div class="adf-search-container" [attr.state]="subscriptAnimationState.value">
|
<div *ngIf="isLoggedIn()">
|
||||||
<div *ngIf="isLoggedIn()" [@transitionMessages]="subscriptAnimationState"
|
<adf-search-text-input #searchTextInput
|
||||||
(@transitionMessages.done)="applySearchFocus($event)">
|
[searchTerm]= "searchTerm"
|
||||||
<button mat-icon-button
|
[expandable]="expandable"
|
||||||
*ngIf="expandable"
|
[autocomplete]="autocomplete"
|
||||||
id="adf-search-button"
|
[liveSearchEnabled]="liveSearchEnabled"
|
||||||
class="adf-search-button"
|
[focusListener]="focusSubject.asObservable()"
|
||||||
[title]="'SEARCH.BUTTON.TOOLTIP' | translate"
|
|
||||||
(click)="toggleSearchBar()"
|
|
||||||
(keyup.enter)="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"
|
|
||||||
[attr.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]="auto"
|
[searchAutocomplete]="auto"
|
||||||
(keyup.enter)="searchSubmit($event)">
|
(searchChange)="inputChange($event)"
|
||||||
</mat-form-field>
|
(reset)="onReset($event)"
|
||||||
</div>
|
(selectResult)="onSelectFirstResult()"
|
||||||
</div>
|
(submit)="submit.emit($event)"></adf-search-text-input>
|
||||||
|
|
||||||
<adf-search #search
|
<adf-search #search
|
||||||
#auto="searchAutocomplete"
|
#auto="searchAutocomplete"
|
||||||
class="adf-search-result-autocomplete"
|
class="adf-search-result-autocomplete"
|
||||||
[maxResults]="liveSearchMaxResults">
|
[maxResults]="liveSearchMaxResults">
|
||||||
<ng-template let-data>
|
<ng-template let-data>
|
||||||
<mat-list *ngIf="isSearchBarActive()" id="autocomplete-search-result-list">
|
<mat-list *ngIf="searchTextInput?.isSearchBarActive()" id="autocomplete-search-result-list">
|
||||||
<mat-list-item
|
<mat-list-item
|
||||||
*ngFor="let item of data?.list?.entries; let idx = index"
|
*ngFor="let item of data?.list?.entries; let idx = index"
|
||||||
id="result_option_{{idx}}"
|
id="result_option_{{idx}}"
|
||||||
[attr.data-automation-id]="'autocomplete_for_' + item.entry.name"
|
[attr.data-automation-id]="'autocomplete_for_' + item.entry.name"
|
||||||
[tabindex]="0"
|
[tabindex]="0"
|
||||||
(focus)="onFocus($event)"
|
|
||||||
(blur)="onBlur($event)"
|
(blur)="onBlur($event)"
|
||||||
(keyup.arrowdown)="onRowArrowDown($event)"
|
(keyup.arrowdown)="onRowArrowDown($event)"
|
||||||
(keyup.arrowup)="onRowArrowUp($event)"
|
(keyup.arrowup)="onRowArrowUp($event)"
|
||||||
@@ -79,3 +60,4 @@
|
|||||||
</mat-list>
|
</mat-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</adf-search>
|
</adf-search>
|
||||||
|
</div>
|
||||||
|
@@ -7,38 +7,8 @@
|
|||||||
$mat-menu-overlay-min-width: 112px !default; // 56 * 2
|
$mat-menu-overlay-min-width: 112px !default; // 56 * 2
|
||||||
$mat-menu-overlay-max-width: 280px !default; // 56 * 5
|
$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 {
|
.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 {
|
&-search-result-autocomplete {
|
||||||
@include mat-overridable-elevation(2);
|
@include mat-overridable-elevation(2);
|
||||||
|
|
||||||
@@ -74,8 +44,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.adf-highlight {
|
|
||||||
color: mat-color($primary, 900);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -16,19 +16,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, DebugElement, ViewChild } from '@angular/core';
|
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 { By } from '@angular/platform-browser';
|
||||||
import {
|
import {
|
||||||
AuthenticationService,
|
AuthenticationService,
|
||||||
SearchService,
|
SearchService,
|
||||||
setupTestBed,
|
setupTestBed,
|
||||||
CoreModule,
|
CoreModule,
|
||||||
UserPreferencesService
|
UserPreferencesService,
|
||||||
|
SearchTextInputComponent
|
||||||
} from '@alfresco/adf-core';
|
} from '@alfresco/adf-core';
|
||||||
import { ThumbnailService } from '@alfresco/adf-core';
|
import { ThumbnailService } from '@alfresco/adf-core';
|
||||||
import { noResult, results } from '../../mock';
|
import { noResult, results } from '../../mock';
|
||||||
import { SearchControlComponent } from './search-control.component';
|
import { SearchControlComponent } from './search-control.component';
|
||||||
import { SearchTriggerDirective } from './search-trigger.directive';
|
|
||||||
import { SearchComponent } from './search.component';
|
import { SearchComponent } from './search.component';
|
||||||
import { EmptySearchResultComponent } from './empty-search-result.component';
|
import { EmptySearchResultComponent } from './empty-search-result.component';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
@@ -51,6 +51,9 @@ export class SimpleSearchTestCustomEmptyComponent {
|
|||||||
@ViewChild(SearchControlComponent)
|
@ViewChild(SearchControlComponent)
|
||||||
searchComponent: SearchControlComponent;
|
searchComponent: SearchControlComponent;
|
||||||
|
|
||||||
|
@ViewChild(SearchTextInputComponent)
|
||||||
|
searchTextInputComponent: SearchTextInputComponent;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +85,6 @@ describe('SearchControlComponent', () => {
|
|||||||
declarations: [
|
declarations: [
|
||||||
SearchControlComponent,
|
SearchControlComponent,
|
||||||
SearchComponent,
|
SearchComponent,
|
||||||
SearchTriggerDirective,
|
|
||||||
EmptySearchResultComponent,
|
EmptySearchResultComponent,
|
||||||
SimpleSearchTestCustomEmptyComponent
|
SimpleSearchTestCustomEmptyComponent
|
||||||
],
|
],
|
||||||
@@ -104,6 +106,7 @@ describe('SearchControlComponent', () => {
|
|||||||
element = fixture.nativeElement;
|
element = fixture.nativeElement;
|
||||||
|
|
||||||
searchServiceSpy = spyOn(searchService, 'search').and.returnValue(of(''));
|
searchServiceSpy = spyOn(searchService, 'search').and.returnValue(of(''));
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -139,24 +142,8 @@ describe('SearchControlComponent', () => {
|
|||||||
fixture.detectChanges();
|
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) => {
|
it('should NOT update FAYT term when user inputs an empty string as search term ', (done) => {
|
||||||
typeWordIntoSearchInput('');
|
typeWordIntoSearchInput('');
|
||||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
|
||||||
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
||||||
|
|
||||||
fixture.detectChanges();
|
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', () => {
|
describe('component rendering', () => {
|
||||||
|
|
||||||
it('should display a text input field by default', async(() => {
|
it('should display a text input field by default', async(() => {
|
||||||
@@ -215,18 +185,6 @@ describe('SearchControlComponent', () => {
|
|||||||
expect(attr).toBe('off');
|
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', () => {
|
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) => {
|
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))));
|
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
||||||
|
spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
typeWordIntoSearchInput('TEST');
|
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) => {
|
it('should show autocomplete list no results when search box has focus and there is search result with length 0', (done) => {
|
||||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true);
|
||||||
searchServiceSpy.and.returnValue(of(noResult));
|
searchServiceSpy.and.returnValue(of(noResult));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
@@ -271,8 +229,8 @@ describe('SearchControlComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should hide autocomplete list results when the search box loses focus', (done) => {
|
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))));
|
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
||||||
|
spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const inputDebugElement = debugElement.query(By.css('#adf-control-input'));
|
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) => {
|
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))));
|
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
||||||
|
spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const inputDebugElement = debugElement.query(By.css('#adf-control-input'));
|
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) => {
|
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))));
|
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
||||||
|
spyOn(component.searchTextInput, 'isSearchBarActive').and.returnValue(true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const inputDebugElement = debugElement.query(By.css('#adf-control-input'));
|
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) => {
|
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))));
|
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
@@ -361,7 +319,7 @@ describe('SearchControlComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should focus input element when autocomplete list is cancelled', (done) => {
|
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))));
|
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
||||||
fixture.detectChanges();
|
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', () => {
|
describe('option click', () => {
|
||||||
|
|
||||||
it('should emit a option clicked event when item is clicked', (done) => {
|
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))));
|
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
||||||
const clickDisposable = component.optionClicked.subscribe((item) => {
|
const clickDisposable = component.optionClicked.subscribe((item) => {
|
||||||
expect(item.entry.id).toBe('123');
|
expect(item.entry.id).toBe('123');
|
||||||
@@ -512,10 +372,10 @@ describe('SearchControlComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set deactivate the search after element is clicked', (done) => {
|
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))));
|
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
||||||
const clickDisposable = component.optionClicked.subscribe(() => {
|
const clickDisposable = component.optionClicked.subscribe(() => {
|
||||||
expect(component.subscriptAnimationState.value).toBe('inactive');
|
expect(component.searchTextInput.subscriptAnimationState.value).toBe('inactive');
|
||||||
clickDisposable.unsubscribe();
|
clickDisposable.unsubscribe();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -531,11 +391,11 @@ describe('SearchControlComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should NOT reset the search term after element is clicked', (done) => {
|
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))));
|
searchServiceSpy.and.returnValue(of(JSON.parse(JSON.stringify(results))));
|
||||||
const clickDisposable = component.optionClicked.subscribe(() => {
|
const clickDisposable = component.optionClicked.subscribe(() => {
|
||||||
expect(component.searchTerm).not.toBeFalsy();
|
expect(component.searchTextInput.searchTerm).not.toBeFalsy();
|
||||||
expect(component.searchTerm).toBe('TEST');
|
expect(component.searchTextInput.searchTerm).toBe('TEST');
|
||||||
clickDisposable.unsubscribe();
|
clickDisposable.unsubscribe();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -557,13 +417,16 @@ describe('SearchControlComponent', () => {
|
|||||||
fixtureCustom = TestBed.createComponent(SimpleSearchTestCustomEmptyComponent);
|
fixtureCustom = TestBed.createComponent(SimpleSearchTestCustomEmptyComponent);
|
||||||
componentCustom = fixtureCustom.componentInstance;
|
componentCustom = fixtureCustom.componentInstance;
|
||||||
elementCustom = fixtureCustom.nativeElement;
|
elementCustom = fixtureCustom.nativeElement;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display the custom no results when it is configured', (done) => {
|
it('should display the custom no results when it is configured', (done) => {
|
||||||
const noResultCustomMessage = 'BANDI IS NOTHING';
|
const noResultCustomMessage = 'BANDI IS NOTHING';
|
||||||
spyOn(componentCustom.searchComponent, 'isSearchBarActive').and.returnValue(true);
|
spyOn(componentCustom.searchComponent, 'isLoggedIn').and.returnValue(true);
|
||||||
componentCustom.setCustomMessageForNoResult(noResultCustomMessage);
|
fixtureCustom.detectChanges();
|
||||||
|
spyOn(componentCustom.searchComponent.searchTextInput, 'isSearchBarActive').and.returnValue(true);
|
||||||
searchServiceSpy.and.returnValue(of(noResult));
|
searchServiceSpy.and.returnValue(of(noResult));
|
||||||
|
componentCustom.setCustomMessageForNoResult(noResultCustomMessage);
|
||||||
fixtureCustom.detectChanges();
|
fixtureCustom.detectChanges();
|
||||||
|
|
||||||
const inputDebugElement = fixtureCustom.debugElement.query(By.css('#adf-control-input'));
|
const inputDebugElement = fixtureCustom.debugElement.query(By.css('#adf-control-input'));
|
||||||
@@ -589,82 +452,14 @@ describe('SearchControlComponent', () => {
|
|||||||
it('should have positive transform translation', () => {
|
it('should have positive transform translation', () => {
|
||||||
userPreferencesService.setWithoutStore('textOrientation', 'ltr');
|
userPreferencesService.setWithoutStore('textOrientation', 'ltr');
|
||||||
fixture.detectChanges();
|
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 ', () => {
|
it('should have negative transform translation ', () => {
|
||||||
userPreferencesService.setWithoutStore('textOrientation', 'rtl');
|
userPreferencesService.setWithoutStore('textOrientation', 'rtl');
|
||||||
fixture.detectChanges();
|
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();
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -15,33 +15,23 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AuthenticationService, ThumbnailService, UserPreferencesService } from '@alfresco/adf-core';
|
import { AuthenticationService, ThumbnailService, SearchTextInputComponent } from '@alfresco/adf-core';
|
||||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output,
|
import { Component, EventEmitter, Input, OnDestroy, Output,
|
||||||
QueryList, ViewEncapsulation, ViewChild, ViewChildren, ElementRef, TemplateRef, ContentChild } from '@angular/core';
|
QueryList, ViewEncapsulation, ViewChild, ViewChildren, TemplateRef, ContentChild } from '@angular/core';
|
||||||
import { NodeEntry } from '@alfresco/js-api';
|
import { NodeEntry } from '@alfresco/js-api';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { SearchComponent } from './search.component';
|
import { SearchComponent } from './search.component';
|
||||||
import { searchAnimation } from './animations';
|
|
||||||
import { MatListItem } from '@angular/material';
|
import { MatListItem } from '@angular/material';
|
||||||
import { EmptySearchResultComponent } from './empty-search-result.component';
|
import { EmptySearchResultComponent } from './empty-search-result.component';
|
||||||
import { debounceTime, filter, takeUntil } from 'rxjs/operators';
|
|
||||||
import { Direction } from '@angular/cdk/bidi';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'adf-search-control',
|
selector: 'adf-search-control',
|
||||||
templateUrl: './search-control.component.html',
|
templateUrl: './search-control.component.html',
|
||||||
styleUrls: ['./search-control.component.scss'],
|
styleUrls: ['./search-control.component.scss'],
|
||||||
animations: [searchAnimation],
|
|
||||||
encapsulation: ViewEncapsulation.None,
|
encapsulation: ViewEncapsulation.None,
|
||||||
host: { class: 'adf-search-control' }
|
host: { class: 'adf-search-control' }
|
||||||
})
|
})
|
||||||
export class SearchControlComponent implements OnInit, OnDestroy {
|
export class SearchControlComponent implements OnDestroy {
|
||||||
|
|
||||||
/** Toggles whether to use an expanding search control. If false
|
|
||||||
* then a regular input is used.
|
|
||||||
*/
|
|
||||||
@Input()
|
|
||||||
expandable: boolean = true;
|
|
||||||
|
|
||||||
/** Toggles highlighting of the search term in the results. */
|
/** Toggles highlighting of the search term in the results. */
|
||||||
@Input()
|
@Input()
|
||||||
@@ -51,13 +41,19 @@ export class SearchControlComponent implements OnInit, OnDestroy {
|
|||||||
@Input()
|
@Input()
|
||||||
inputType: string = 'text';
|
inputType: string = 'text';
|
||||||
|
|
||||||
|
/** Toggles "find-as-you-type" suggestions for possible matches. */
|
||||||
|
@Input()
|
||||||
|
liveSearchEnabled: boolean = true;
|
||||||
|
|
||||||
/** Toggles auto-completion of the search input field. */
|
/** Toggles auto-completion of the search input field. */
|
||||||
@Input()
|
@Input()
|
||||||
autocomplete: boolean = false;
|
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()
|
@Input()
|
||||||
liveSearchEnabled: boolean = true;
|
expandable: boolean = true;
|
||||||
|
|
||||||
/** Maximum number of results to show in the live search. */
|
/** Maximum number of results to show in the live search. */
|
||||||
@Input()
|
@Input()
|
||||||
@@ -81,87 +77,34 @@ export class SearchControlComponent implements OnInit, OnDestroy {
|
|||||||
@Output()
|
@Output()
|
||||||
optionClicked: EventEmitter<any> = new EventEmitter();
|
optionClicked: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
@ViewChild('searchTextInput')
|
||||||
|
searchTextInput: SearchTextInputComponent;
|
||||||
|
|
||||||
@ViewChild('search')
|
@ViewChild('search')
|
||||||
searchAutocomplete: SearchComponent;
|
searchAutocomplete: SearchComponent;
|
||||||
|
|
||||||
@ViewChild('searchInput')
|
|
||||||
searchInput: ElementRef;
|
|
||||||
|
|
||||||
@ViewChildren(MatListItem)
|
@ViewChildren(MatListItem)
|
||||||
private listResultElement: QueryList<MatListItem>;
|
private listResultElement: QueryList<MatListItem>;
|
||||||
|
|
||||||
@ContentChild(EmptySearchResultComponent)
|
@ContentChild(EmptySearchResultComponent)
|
||||||
emptySearchTemplate: EmptySearchResultComponent;
|
emptySearchTemplate: EmptySearchResultComponent;
|
||||||
|
|
||||||
searchTerm: string = '';
|
focusSubject = new Subject<FocusEvent>();
|
||||||
subscriptAnimationState: any;
|
|
||||||
noSearchResultTemplate: TemplateRef <any> = null;
|
noSearchResultTemplate: TemplateRef <any> = null;
|
||||||
|
searchTerm: string = '';
|
||||||
|
|
||||||
private toggleSearch = new Subject<any>();
|
|
||||||
private focusSubject = new Subject<FocusEvent>();
|
|
||||||
private onDestroy$ = new Subject<boolean>();
|
private onDestroy$ = new Subject<boolean>();
|
||||||
private dir = 'ltr';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public authService: AuthenticationService,
|
public authService: AuthenticationService,
|
||||||
private thumbnailService: ThumbnailService,
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
isNoSearchTemplatePresent(): boolean {
|
isNoSearchTemplatePresent(): boolean {
|
||||||
return this.emptySearchTemplate ? true : false;
|
return this.emptySearchTemplate ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
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$.next(true);
|
||||||
this.onDestroy$.complete();
|
this.onDestroy$.complete();
|
||||||
}
|
}
|
||||||
@@ -170,17 +113,9 @@ export class SearchControlComponent implements OnInit, OnDestroy {
|
|||||||
return this.authService.isEcmLoggedIn();
|
return this.authService.isEcmLoggedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
searchSubmit(event: any) {
|
inputChange(value: string) {
|
||||||
this.submit.emit(event);
|
this.searchTerm = value;
|
||||||
this.toggleSearchBar();
|
this.searchChange.emit(value);
|
||||||
}
|
|
||||||
|
|
||||||
inputChange(event: any) {
|
|
||||||
this.searchChange.emit(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAutoComplete(): string {
|
|
||||||
return this.autocomplete ? 'on' : 'off';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMimeTypeIcon(node: NodeEntry): string {
|
getMimeTypeIcon(node: NodeEntry): string {
|
||||||
@@ -200,20 +135,10 @@ export class SearchControlComponent implements OnInit, OnDestroy {
|
|||||||
return mimeType;
|
return mimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
isSearchBarActive() {
|
|
||||||
return this.subscriptAnimationState.value === 'active' && this.liveSearchEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSearchBar() {
|
|
||||||
if (this.toggleSearch) {
|
|
||||||
this.toggleSearch.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
elementClicked(item: any) {
|
elementClicked(item: any) {
|
||||||
if (item.entry) {
|
if (item.entry) {
|
||||||
this.optionClicked.next(item);
|
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 {
|
onBlur($event): void {
|
||||||
|
const nextElement: any = this.getNextElementSibling(<Element> $event.target);
|
||||||
|
if (!nextElement && !this.isListElement($event)) {
|
||||||
this.focusSubject.next($event);
|
this.focusSubject.next($event);
|
||||||
}
|
}
|
||||||
|
|
||||||
activateToolbar() {
|
|
||||||
if (!this.isSearchBarActive()) {
|
|
||||||
this.toggleSearchBar();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectFirstResult() {
|
onSelectFirstResult() {
|
||||||
if ( this.listResultElement && this.listResultElement.length > 0) {
|
if ( this.listResultElement && this.listResultElement.length > 0) {
|
||||||
const firstElement: MatListItem = <MatListItem> this.listResultElement.first;
|
const firstElement: MatListItem = <MatListItem> this.listResultElement.first;
|
||||||
firstElement._getHostElement().focus();
|
firstElement._getHostElement().focus();
|
||||||
@@ -250,24 +172,18 @@ export class SearchControlComponent implements OnInit, OnDestroy {
|
|||||||
if (previousElement) {
|
if (previousElement) {
|
||||||
previousElement.focus();
|
previousElement.focus();
|
||||||
} else {
|
} else {
|
||||||
this.searchInput.nativeElement.focus();
|
|
||||||
this.focusSubject.next(new FocusEvent('focus'));
|
this.focusSubject.next(new FocusEvent('focus'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupFocusEventHandlers() {
|
onReset(status: boolean) {
|
||||||
const focusEvents: Observable<FocusEvent> = this.focusSubject
|
if (status) {
|
||||||
.pipe(
|
this.searchAutocomplete.resetResults();
|
||||||
debounceTime(50),
|
}
|
||||||
filter(($event: any) => {
|
}
|
||||||
return this.isSearchBarActive() && ($event.type === 'blur' || $event.type === 'focusout');
|
|
||||||
}),
|
|
||||||
takeUntil(this.onDestroy$)
|
|
||||||
);
|
|
||||||
|
|
||||||
focusEvents.subscribe(() => {
|
private isListElement($event: any): boolean {
|
||||||
this.toggleSearchBar();
|
return $event.relatedTarget && $event.relatedTarget.children[0].className === 'mat-list-item-content';
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNextElementSibling(node: Element): Element {
|
private getNextElementSibling(node: Element): Element {
|
||||||
@@ -277,28 +193,4 @@ export class SearchControlComponent implements OnInit, OnDestroy {
|
|||||||
private getPreviousElementSibling(node: Element): Element {
|
private getPreviousElementSibling(node: Element): Element {
|
||||||
return node.previousElementSibling;
|
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' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SearchService } from '@alfresco/adf-core';
|
import { SearchService, SearchComponentInterface } from '@alfresco/adf-core';
|
||||||
import {
|
import {
|
||||||
AfterContentInit,
|
AfterContentInit,
|
||||||
Component,
|
Component,
|
||||||
@@ -45,7 +45,7 @@ import { debounceTime, takeUntil } from 'rxjs/operators';
|
|||||||
'class': 'adf-search'
|
'class': 'adf-search'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export class SearchComponent implements AfterContentInit, OnChanges, OnDestroy {
|
export class SearchComponent implements SearchComponentInterface, AfterContentInit, OnChanges, OnDestroy {
|
||||||
|
|
||||||
@ViewChild('panel')
|
@ViewChild('panel')
|
||||||
panel: ElementRef;
|
panel: ElementRef;
|
||||||
|
@@ -28,7 +28,6 @@ export { SearchRange } from './search-range.interface';
|
|||||||
|
|
||||||
export * from './components/search.component';
|
export * from './components/search.component';
|
||||||
export * from './components/search-control.component';
|
export * from './components/search-control.component';
|
||||||
export * from './components/search-trigger.directive';
|
|
||||||
export * from './components/empty-search-result.component';
|
export * from './components/empty-search-result.component';
|
||||||
export * from './components/search-filter/search-filter.component';
|
export * from './components/search-filter/search-filter.component';
|
||||||
export * from './components/search-filter/search-filter.service';
|
export * from './components/search-filter/search-filter.service';
|
||||||
|
@@ -22,8 +22,6 @@ import { MaterialModule } from '../material.module';
|
|||||||
|
|
||||||
import { CoreModule } from '@alfresco/adf-core';
|
import { CoreModule } from '@alfresco/adf-core';
|
||||||
|
|
||||||
import { SearchTriggerDirective } from './components/search-trigger.directive';
|
|
||||||
|
|
||||||
import { SearchControlComponent } from './components/search-control.component';
|
import { SearchControlComponent } from './components/search-control.component';
|
||||||
import { SearchComponent } from './components/search.component';
|
import { SearchComponent } from './components/search.component';
|
||||||
import { EmptySearchResultComponent } from './components/empty-search-result.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[] = [
|
export const ALFRESCO_SEARCH_DIRECTIVES: any[] = [
|
||||||
SearchComponent,
|
SearchComponent,
|
||||||
SearchControlComponent,
|
SearchControlComponent,
|
||||||
SearchTriggerDirective,
|
|
||||||
EmptySearchResultComponent,
|
EmptySearchResultComponent,
|
||||||
SearchFilterComponent,
|
SearchFilterComponent,
|
||||||
SearchChipListComponent
|
SearchChipListComponent
|
||||||
|
@@ -56,7 +56,7 @@ import { TranslateLoaderService } from './services/translate-loader.service';
|
|||||||
import { ExtensionsModule } from '@alfresco/adf-extensions';
|
import { ExtensionsModule } from '@alfresco/adf-extensions';
|
||||||
import { directionalityConfigFactory } from './services/directionality-config-factory';
|
import { directionalityConfigFactory } from './services/directionality-config-factory';
|
||||||
import { DirectionalityConfigService } from './services/directionality-config.service';
|
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({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -91,7 +91,7 @@ import { SearchCloudModule } from './search-cloud/search-cloud.module';
|
|||||||
IconModule,
|
IconModule,
|
||||||
SortingPickerModule,
|
SortingPickerModule,
|
||||||
NotificationHistoryModule,
|
NotificationHistoryModule,
|
||||||
SearchCloudModule
|
SearchTextModule
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AboutModule,
|
AboutModule,
|
||||||
@@ -125,7 +125,7 @@ import { SearchCloudModule } from './search-cloud/search-cloud.module';
|
|||||||
SortingPickerModule,
|
SortingPickerModule,
|
||||||
IconModule,
|
IconModule,
|
||||||
NotificationHistoryModule,
|
NotificationHistoryModule,
|
||||||
SearchCloudModule
|
SearchTextModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreModule {
|
export class CoreModule {
|
||||||
|
@@ -42,7 +42,7 @@ export * from './clipboard/index';
|
|||||||
export * from './dialogs/index';
|
export * from './dialogs/index';
|
||||||
export * from './icon/index';
|
export * from './icon/index';
|
||||||
export * from './notifications/index';
|
export * from './notifications/index';
|
||||||
export * from './search-cloud/index';
|
export * from './search-text/index';
|
||||||
|
|
||||||
export * from './utils/index';
|
export * from './utils/index';
|
||||||
export * from './interface/index';
|
export * from './interface/index';
|
||||||
|
@@ -15,7 +15,9 @@
|
|||||||
* limitations under the License.
|
* 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 {
|
export interface SearchConfigurationInterface {
|
||||||
|
|
||||||
@@ -29,3 +31,17 @@ export interface SearchConfigurationInterface {
|
|||||||
generateQueryBody(searchTerm: string, maxResults: number, skipCount: number): QueryBody;
|
generateQueryBody(searchTerm: string, maxResults: number, skipCount: number): QueryBody;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchComponentInterface {
|
||||||
|
|
||||||
|
panel: ElementRef;
|
||||||
|
showPanel: boolean;
|
||||||
|
results: NodePaging;
|
||||||
|
isOpen: boolean;
|
||||||
|
keyPressedStream: Subject<string>;
|
||||||
|
displayWith: ((value: any) => string) | null;
|
||||||
|
|
||||||
|
resetResults(): void;
|
||||||
|
hidePanel(): void;
|
||||||
|
setVisibility(): void;
|
||||||
|
}
|
||||||
|
@@ -33,3 +33,4 @@ export * from './identity-group.model';
|
|||||||
export * from './identity-user.model';
|
export * from './identity-user.model';
|
||||||
export * from './identity-role.model';
|
export * from './identity-role.model';
|
||||||
export * from './identity-group.model';
|
export * from './identity-group.model';
|
||||||
|
export * from './search-text-input.model';
|
||||||
|
@@ -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);
|
|
||||||
}
|
|
@@ -15,12 +15,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
export enum SearchTextStateEnum {
|
||||||
import { Subject } from 'rxjs';
|
expanded = 'expanded',
|
||||||
|
collapsed = 'collapsed'
|
||||||
@Injectable({
|
}
|
||||||
providedIn: 'root'
|
|
||||||
})
|
export interface SearchAnimationState {
|
||||||
export class SearchCloudService {
|
value: string;
|
||||||
value = new Subject<String>();
|
params?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAnimationControl {
|
||||||
|
active: SearchAnimationState;
|
||||||
|
inactive: SearchAnimationState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAnimationDirection {
|
||||||
|
ltr: SearchAnimationControl;
|
||||||
|
rtl: SearchAnimationControl;
|
||||||
}
|
}
|
@@ -1,14 +0,0 @@
|
|||||||
<div class="adf-search-cloud-wrapper" #searchContainer>
|
|
||||||
|
|
||||||
<mat-form-field class="adf-search-text-cloud">
|
|
||||||
<input
|
|
||||||
matInput
|
|
||||||
[(ngModel)]="properties.value"
|
|
||||||
placeholder="{{ properties?.placeholder }}"
|
|
||||||
(input)="onChangedHandler($event)"
|
|
||||||
data-automation-id="search-project-field" />
|
|
||||||
<mat-icon matSuffix *ngIf="properties.value?.length > 0" (click)="clear()">close</mat-icon>
|
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
<mat-icon class="adf-search-cloud-icon" (click)="toggle()" matSuffix>search</mat-icon>
|
|
||||||
</div>
|
|
@@ -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<SearchTextCloudComponent>;
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
|
@@ -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<void> = new Subject<void>();
|
|
||||||
|
|
||||||
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 = '';
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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<SearchCloudComponent>;
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
|
@@ -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: '<div #container></div>',
|
|
||||||
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<String> = new EventEmitter<String>();
|
|
||||||
|
|
||||||
private componentRef: ComponentRef<any>;
|
|
||||||
onDestroy$: Subject<void> = new Subject<void>();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -15,5 +15,5 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './search-cloud.component';
|
export * from './search-text-input.component';
|
||||||
export * from './search-cloud.module';
|
export * from './search-text-input.module';
|
30
lib/core/search-text/search-text-input.component.html
Normal file
30
lib/core/search-text/search-text-input.component.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<div class="adf-search-container" [attr.state]="subscriptAnimationState.value">
|
||||||
|
<div [@transitionMessages]="subscriptAnimationState"
|
||||||
|
(@transitionMessages.done)="applySearchFocus($event)">
|
||||||
|
<button mat-icon-button
|
||||||
|
*ngIf="expandable"
|
||||||
|
id="adf-search-button"
|
||||||
|
class="adf-search-button"
|
||||||
|
[title]="'SEARCH.BUTTON.TOOLTIP' | translate"
|
||||||
|
(click)="toggleSearchBar()"
|
||||||
|
(keyup.enter)="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"
|
||||||
|
[attr.type]="inputType"
|
||||||
|
[autocomplete]="getAutoComplete()"
|
||||||
|
id="adf-control-input"
|
||||||
|
[(ngModel)]="searchTerm"
|
||||||
|
(focus)="activateToolbar()"
|
||||||
|
(blur)="onBlur($event)"
|
||||||
|
(keyup.escape)="toggleSearchBar()"
|
||||||
|
(keyup.arrowdown)="selectFirstResult($event)"
|
||||||
|
(ngModelChange)="inputChange($event)"
|
||||||
|
[searchAutocomplete]="searchAutocomplete ? searchAutocomplete : null"
|
||||||
|
(keyup.enter)="searchSubmit($event)">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
46
lib/core/search-text/search-text-input.component.scss
Normal file
46
lib/core/search-text/search-text-input.component.scss
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
248
lib/core/search-text/search-text-input.component.spec.ts
Normal file
248
lib/core/search-text/search-text-input.component.spec.ts
Normal file
@@ -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<SearchTextInputComponent>;
|
||||||
|
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<any>();
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
270
lib/core/search-text/search-text-input.component.ts
Normal file
270
lib/core/search-text/search-text-input.component.ts
Normal file
@@ -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<FocusEvent>;
|
||||||
|
|
||||||
|
@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<string> = 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<any> = new EventEmitter();
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
selectResult: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
reset: EventEmitter<boolean> = 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<boolean>();
|
||||||
|
private toggleSearch = new Subject<any>();
|
||||||
|
private focusSubscription: Subscription;
|
||||||
|
private valueChange = new Subject<string>();
|
||||||
|
|
||||||
|
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<FocusEvent> = 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();
|
||||||
|
}
|
||||||
|
}
|
@@ -16,23 +16,27 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
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 { CommonModule } from '@angular/common';
|
||||||
import { MaterialModule } from '../material.module';
|
import { MaterialModule } from '../material.module';
|
||||||
import { FormsModule } from '@angular/forms';
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
SearchCloudComponent,
|
SearchTextInputComponent,
|
||||||
SearchTextCloudComponent
|
SearchTriggerDirective
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
MaterialModule,
|
MaterialModule,
|
||||||
FormsModule
|
FormsModule
|
||||||
],
|
],
|
||||||
exports: [ SearchCloudComponent],
|
exports: [
|
||||||
entryComponents: [ SearchTextCloudComponent ]
|
SearchTextInputComponent,
|
||||||
|
SearchTriggerDirective
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class SearchCloudModule {}
|
export class SearchTextModule {}
|
@@ -32,8 +32,8 @@ import {
|
|||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { Observable, Subject, Subscription, merge, of, fromEvent } from 'rxjs';
|
import { Observable, Subject, Subscription, merge, of, fromEvent } from 'rxjs';
|
||||||
import { SearchComponent } from './search.component';
|
|
||||||
import { filter, switchMap, takeUntil } from 'rxjs/operators';
|
import { filter, switchMap, takeUntil } from 'rxjs/operators';
|
||||||
|
import { SearchComponentInterface } from '../../core/interface/search-configuration.interface';
|
||||||
|
|
||||||
export const SEARCH_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
|
export const SEARCH_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
|
||||||
provide: NG_VALUE_ACCESSOR,
|
provide: NG_VALUE_ACCESSOR,
|
||||||
@@ -62,7 +62,7 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy {
|
|||||||
private onDestroy$: Subject<boolean> = new Subject<boolean>();
|
private onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||||
|
|
||||||
@Input('searchAutocomplete')
|
@Input('searchAutocomplete')
|
||||||
searchPanel: SearchComponent;
|
searchPanel: SearchComponentInterface;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
autocomplete: string = 'off';
|
autocomplete: string = 'off';
|
||||||
@@ -164,10 +164,10 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy {
|
|||||||
if (document.activeElement === event.target ) {
|
if (document.activeElement === event.target ) {
|
||||||
const inputValue: string = (event.target as HTMLInputElement).value;
|
const inputValue: string = (event.target as HTMLInputElement).value;
|
||||||
this.onChange(inputValue);
|
this.onChange(inputValue);
|
||||||
if (inputValue) {
|
if (inputValue && this.searchPanel) {
|
||||||
this.searchPanel.keyPressedStream.next(inputValue);
|
this.searchPanel.keyPressedStream.next(inputValue);
|
||||||
this.openPanel();
|
this.openPanel();
|
||||||
} else {
|
} else if (this.searchPanel) {
|
||||||
this.searchPanel.resetResults();
|
this.searchPanel.resetResults();
|
||||||
this.closePanel();
|
this.closePanel();
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy {
|
|||||||
|
|
||||||
private isPanelOptionClicked(event: MouseEvent) {
|
private isPanelOptionClicked(event: MouseEvent) {
|
||||||
let isPanelOption: boolean = false;
|
let isPanelOption: boolean = false;
|
||||||
if ( event ) {
|
if ( event && this.searchPanel ) {
|
||||||
const clickTarget = event.target as HTMLElement;
|
const clickTarget = event.target as HTMLElement;
|
||||||
isPanelOption = !this.isNoResultOption() &&
|
isPanelOption = !this.isNoResultOption() &&
|
||||||
!!this.searchPanel.panel &&
|
!!this.searchPanel.panel &&
|
||||||
@@ -185,8 +185,8 @@ export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy {
|
|||||||
return isPanelOption;
|
return isPanelOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isNoResultOption() {
|
private isNoResultOption(): boolean {
|
||||||
return this.searchPanel.results.list ? this.searchPanel.results.list.entries.length === 0 : true;
|
return this.searchPanel && this.searchPanel.results.list ? this.searchPanel.results.list.entries.length === 0 : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToClosingActions(): Subscription {
|
private subscribeToClosingActions(): Subscription {
|
@@ -33,6 +33,7 @@
|
|||||||
@import '../login/components/login-dialog.component';
|
@import '../login/components/login-dialog.component';
|
||||||
@import '../login/components/login-dialog-panel.component';
|
@import '../login/components/login-dialog-panel.component';
|
||||||
@import '../../core/clipboard/clipboard.component';
|
@import '../../core/clipboard/clipboard.component';
|
||||||
|
@import '../../core/search-text/search-text-input.component';
|
||||||
@import './snackbar';
|
@import './snackbar';
|
||||||
|
|
||||||
@mixin adf-core-theme($theme) {
|
@mixin adf-core-theme($theme) {
|
||||||
@@ -71,4 +72,5 @@
|
|||||||
@include adf-clipboard-theme($theme);
|
@include adf-clipboard-theme($theme);
|
||||||
@include adf-snackbar-theme($theme);
|
@include adf-snackbar-theme($theme);
|
||||||
@include mat-expansion-panel-theme--fix($theme);
|
@include mat-expansion-panel-theme--fix($theme);
|
||||||
|
@include adf-search-text-input-theme($theme);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user