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
33
lib/core/search-text/animations.ts
Normal file
33
lib/core/search-text/animations.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*!
|
||||
* @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 { trigger, transition, animate, style, state, AnimationTriggerMetadata } from '@angular/animations';
|
||||
|
||||
export const searchAnimation: AnimationTriggerMetadata = trigger('transitionMessages', [
|
||||
state('active', style({
|
||||
'margin-left': '{{ margin-left }}px',
|
||||
'margin-right': '{{ margin-right }}px',
|
||||
'transform': '{{ transform }}'
|
||||
}), { params: { 'margin-left': 0, 'margin-right': 0, 'transform': 'translateX(0%)' } }),
|
||||
state('inactive', style({
|
||||
'margin-left': '{{ margin-left }}px',
|
||||
'margin-right': '{{ margin-right }}px',
|
||||
'transform': '{{ transform }}'
|
||||
}), { params: { 'margin-left': 0, 'margin-right': 0, 'transform': 'translateX(0%)' } }),
|
||||
state('no-animation', style({ transform: 'translateX(0%)', width: '100%' })),
|
||||
transition('active <=> inactive', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)'))
|
||||
]);
|
18
lib/core/search-text/index.ts
Normal file
18
lib/core/search-text/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*!
|
||||
* @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.
|
||||
*/
|
||||
|
||||
export * from './public-api';
|
19
lib/core/search-text/public-api.ts
Normal file
19
lib/core/search-text/public-api.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*!
|
||||
* @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.
|
||||
*/
|
||||
|
||||
export * from './search-text-input.component';
|
||||
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();
|
||||
}
|
||||
}
|
42
lib/core/search-text/search-text-input.module.ts
Normal file
42
lib/core/search-text/search-text-input.module.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*!
|
||||
* @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 { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SearchTextInputComponent } from './search-text-input.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchTriggerDirective } from './search-trigger.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
SearchTextInputComponent,
|
||||
SearchTriggerDirective
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TranslateModule.forChild(),
|
||||
MaterialModule,
|
||||
FormsModule
|
||||
],
|
||||
exports: [
|
||||
SearchTextInputComponent,
|
||||
SearchTriggerDirective
|
||||
]
|
||||
})
|
||||
export class SearchTextModule {}
|
222
lib/core/search-text/search-trigger.directive.ts
Normal file
222
lib/core/search-text/search-trigger.directive.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/*!
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/* tslint:disable: no-input-rename no-use-before-declare no-input-rename */
|
||||
|
||||
import { ENTER, ESCAPE } from '@angular/cdk/keycodes';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Directive,
|
||||
ElementRef,
|
||||
forwardRef,
|
||||
Inject,
|
||||
Input,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
Optional
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Observable, Subject, Subscription, merge, of, fromEvent } from 'rxjs';
|
||||
import { filter, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { SearchComponentInterface } from '../../core/interface/search-configuration.interface';
|
||||
|
||||
export const SEARCH_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SearchTriggerDirective),
|
||||
multi: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Directive selectors without adf- prefix will be deprecated on 3.0.0
|
||||
*/
|
||||
@Directive({
|
||||
// tslint:disable-next-line:directive-selector
|
||||
selector: `input[searchAutocomplete], textarea[searchAutocomplete]`,
|
||||
host: {
|
||||
'role': 'combobox',
|
||||
'[attr.autocomplete]': 'autocomplete',
|
||||
'aria-autocomplete': 'list',
|
||||
'[attr.aria-expanded]': 'panelOpen.toString()',
|
||||
'(blur)': 'onTouched()',
|
||||
'(input)': 'handleInput($event)',
|
||||
'(keydown)': 'handleKeydown($event)'
|
||||
},
|
||||
providers: [SEARCH_AUTOCOMPLETE_VALUE_ACCESSOR]
|
||||
})
|
||||
export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy {
|
||||
private onDestroy$: Subject<boolean> = new Subject<boolean>();
|
||||
|
||||
@Input('searchAutocomplete')
|
||||
searchPanel: SearchComponentInterface;
|
||||
|
||||
@Input()
|
||||
autocomplete: string = 'off';
|
||||
|
||||
private _panelOpen: boolean = false;
|
||||
private closingActionsSubscription: Subscription;
|
||||
private escapeEventStream = new Subject<void>();
|
||||
|
||||
onChange: (value: any) => void = () => { };
|
||||
|
||||
onTouched = () => { };
|
||||
|
||||
constructor(private element: ElementRef,
|
||||
private ngZone: NgZone,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@Optional() @Inject(DOCUMENT) private document: any) { }
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
|
||||
if (this.escapeEventStream) {
|
||||
this.escapeEventStream = null;
|
||||
}
|
||||
if ( this.closingActionsSubscription ) {
|
||||
this.closingActionsSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
get panelOpen(): boolean {
|
||||
return this._panelOpen && this.searchPanel.showPanel;
|
||||
}
|
||||
|
||||
openPanel(): void {
|
||||
this.searchPanel.isOpen = this._panelOpen = true;
|
||||
this.closingActionsSubscription = this.subscribeToClosingActions();
|
||||
}
|
||||
|
||||
closePanel(): void {
|
||||
if (this._panelOpen) {
|
||||
this.closingActionsSubscription.unsubscribe();
|
||||
this._panelOpen = false;
|
||||
this.searchPanel.resetResults();
|
||||
this.searchPanel.hidePanel();
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
get panelClosingActions(): Observable<any> {
|
||||
return merge(
|
||||
this.escapeEventStream,
|
||||
this.outsideClickStream
|
||||
);
|
||||
}
|
||||
|
||||
private get outsideClickStream(): Observable<any> {
|
||||
if (!this.document) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return merge(
|
||||
fromEvent(this.document, 'click'),
|
||||
fromEvent(this.document, 'touchend')
|
||||
).pipe(
|
||||
filter((event: MouseEvent | TouchEvent) => {
|
||||
const clickTarget = event.target as HTMLElement;
|
||||
return this._panelOpen && clickTarget !== this.element.nativeElement;
|
||||
}),
|
||||
takeUntil(this.onDestroy$)
|
||||
);
|
||||
}
|
||||
|
||||
writeValue(value: any): void {
|
||||
Promise.resolve(null).then(() => this.setTriggerValue(value));
|
||||
}
|
||||
|
||||
registerOnChange(fn: (value: any) => {}): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => {}) {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
handleKeydown(event: KeyboardEvent): void {
|
||||
const keyCode = event.keyCode;
|
||||
|
||||
if (keyCode === ESCAPE && this.panelOpen) {
|
||||
this.escapeEventStream.next();
|
||||
event.stopPropagation();
|
||||
} else if (keyCode === ENTER) {
|
||||
this.escapeEventStream.next();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
handleInput(event: KeyboardEvent): void {
|
||||
if (document.activeElement === event.target ) {
|
||||
const inputValue: string = (event.target as HTMLInputElement).value;
|
||||
this.onChange(inputValue);
|
||||
if (inputValue && this.searchPanel) {
|
||||
this.searchPanel.keyPressedStream.next(inputValue);
|
||||
this.openPanel();
|
||||
} else if (this.searchPanel) {
|
||||
this.searchPanel.resetResults();
|
||||
this.closePanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isPanelOptionClicked(event: MouseEvent) {
|
||||
let isPanelOption: boolean = false;
|
||||
if ( event && this.searchPanel ) {
|
||||
const clickTarget = event.target as HTMLElement;
|
||||
isPanelOption = !this.isNoResultOption() &&
|
||||
!!this.searchPanel.panel &&
|
||||
!!this.searchPanel.panel.nativeElement.contains(clickTarget);
|
||||
}
|
||||
return isPanelOption;
|
||||
}
|
||||
|
||||
private isNoResultOption(): boolean {
|
||||
return this.searchPanel && this.searchPanel.results.list ? this.searchPanel.results.list.entries.length === 0 : true;
|
||||
}
|
||||
|
||||
private subscribeToClosingActions(): Subscription {
|
||||
const firstStable = this.ngZone.onStable.asObservable();
|
||||
const optionChanges = this.searchPanel.keyPressedStream.asObservable();
|
||||
|
||||
return merge(firstStable, optionChanges)
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
this.searchPanel.setVisibility();
|
||||
return this.panelClosingActions;
|
||||
}),
|
||||
takeUntil(this.onDestroy$)
|
||||
)
|
||||
.subscribe((event) => this.setValueAndClose(event));
|
||||
}
|
||||
|
||||
private setTriggerValue(value: any): void {
|
||||
const toDisplay = this.searchPanel && this.searchPanel.displayWith ?
|
||||
this.searchPanel.displayWith(value) : value;
|
||||
const inputValue = toDisplay != null ? toDisplay : '';
|
||||
this.element.nativeElement.value = inputValue;
|
||||
}
|
||||
|
||||
private setValueAndClose(event: any | null): void {
|
||||
if (this.isPanelOptionClicked(event) && !event.defaultPrevented) {
|
||||
this.setTriggerValue(event.target.textContent.trim());
|
||||
this.onChange(event.target.textContent.trim());
|
||||
this.element.nativeElement.focus();
|
||||
}
|
||||
this.closePanel();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user