Move search-input-text from content-services to core (#8239)

* Move search-input-text from content-services to core

* Move SearchConfigurationInterface into the content-services since is content related

* Rename search-configuration.interface file to search-component.interface

* Update import SearchConfigurationInterface from content-services

* Decouple SearchComponentInterface from js-api

* Decaplre NodeEntry instead using type any

* Remove comment

* Update component import to fix unit test

* Import SearchTextModule into core
This commit is contained in:
Amedeo Lepore
2023-02-06 18:40:28 +01:00
committed by GitHub
parent c12b4284ef
commit 4c7e500ea8
28 changed files with 73 additions and 36 deletions

View File

@@ -15,9 +15,7 @@
* limitations under the License.
*/
import { QueryBody, NodePaging } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { ElementRef } from '@angular/core';
import { QueryBody } from '@alfresco/js-api';
export interface SearchConfigurationInterface {
@@ -32,17 +30,3 @@ export interface SearchConfigurationInterface {
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;
}

View File

@@ -18,4 +18,5 @@
export * from './services/favorites-api.service';
export * from './services/card-view-content-update.service';
export * from './services/sites.service';
export * from './interface/search-configuration.interface';
export * from './interfaces/search-configuration.interface';

View File

@@ -18,7 +18,7 @@
import { CommonModule } from '@angular/common';
import { NgModule, ModuleWithProviders, APP_INITIALIZER } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CoreModule, TRANSLATION_PROVIDER } from '@alfresco/adf-core';
import { CoreModule, TRANSLATION_PROVIDER, SearchTextModule } from '@alfresco/adf-core';
import { MaterialModule } from './material.module';
@@ -47,7 +47,6 @@ import { VersionCompatibilityService } from './version-compatibility/version-com
import { ContentPipeModule } from './pipes/content-pipe.module';
import { NodeCommentsModule } from './node-comments/node-comments.module';
import { TreeModule } from './tree/tree.module';
import { SearchTextModule } from './search-text/search-text-input.module';
import { AlfrescoViewerModule } from './viewer/alfresco-viewer.module';
@NgModule({

View File

@@ -16,8 +16,8 @@
*/
import { QueryBody } from '@alfresco/js-api';
import { SearchConfigurationInterface } from '../../../common/interface/search-configuration.interface';
import { Injectable, Optional, Inject, InjectionToken } from '@angular/core';
import { SearchConfigurationInterface } from '../../../common/interfaces/search-configuration.interface';
export const SEARCH_QUERY_TOKEN = new InjectionToken<QueryProvider>('Alfresco Search Query Token');
export interface QueryProvider {

View File

@@ -1,33 +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 { 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)'))
]);

View File

@@ -1,18 +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.
*/
export * from './public-api';

View File

@@ -1,37 +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.
*/
// eslint-disable-next-line no-shadow
export enum SearchTextStateEnum {
expanded = 'expanded',
collapsed = 'collapsed'
}
export interface SearchAnimationState {
value: string;
params?: any;
}
export interface SearchAnimationControl {
active: SearchAnimationState;
inactive: SearchAnimationState;
}
export interface SearchAnimationDirection {
ltr: SearchAnimationControl;
rtl: SearchAnimationControl;
}

View File

@@ -1,22 +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.
*/
export * from './animations';
export * from './search-text-input.component';
export * from './search-trigger.directive';
export * from './search-text-input.module';
export * from './models/search-text-input.model';

View File

@@ -1,38 +0,0 @@
<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" [hintLabel]="hintLabel">
<input matInput
#searchInput
[attr.aria-label]="'SEARCH.INPUT.ARIA-LABEL' | translate"
[attr.type]="inputType"
[autocomplete]="getAutoComplete()"
id="adf-control-input"
[(ngModel)]="searchTerm"
[placeholder]="placeholder"
(focus)="activateToolbar()"
(blur)="onBlur($event)"
(keyup.escape)="toggleSearchBar()"
(keyup.arrowdown)="selectFirstResult($event)"
(ngModelChange)="inputChange($event)"
[searchAutocomplete]="searchAutocomplete ? searchAutocomplete : null"
(keyup.enter)="searchSubmit($event)">
<button mat-icon-button matSuffix
data-automation-id="adf-clear-search-button"
class="adf-clear-search-button"
*ngIf="canShowClearSearch()"
(mousedown)="resetSearch()">
<mat-icon>clear</mat-icon>
</button>
</mat-form-field>
</div>
</div>

View File

@@ -1,37 +0,0 @@
.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: var(--adf-search-input-bg-color);
.mat-form-field-ripple {
background-color: var(--adf-search-input-bg-color);
}
}
font-size: var(--theme-subheading-2-font-size);
}
}
.adf-highlight {
color: var(--adf-search-input-highlight-color);
}

View File

@@ -1,384 +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, discardPeriodicTasks, fakeAsync, tick } from '@angular/core/testing';
import { CoreTestingModule, UserPreferencesService, setupTestBed } from '@alfresco/adf-core';
import { SearchTextInputComponent } from './search-text-input.component';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { Subject } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
describe('SearchTextInputComponent', () => {
let fixture: ComponentFixture<SearchTextInputComponent>;
let component: SearchTextInputComponent;
let debugElement: DebugElement;
let element: HTMLElement;
let userPreferencesService: UserPreferencesService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(SearchTextInputComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
element = fixture.nativeElement;
userPreferencesService = TestBed.inject(UserPreferencesService);
component.focusListener = new Subject<any>();
});
afterEach(() => {
fixture.destroy();
});
describe('component rendering', () => {
it('should display a search input field when specified', async () => {
fixture.detectChanges();
await fixture.whenStable();
component.inputType = 'search';
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelectorAll('input[type="search"]').length).toBe(1);
});
});
describe('expandable option false', () => {
beforeEach(() => {
component.expandable = false;
});
it('search button should be hide', () => {
fixture.detectChanges();
const searchButton: any = element.querySelector('#adf-search-button');
expect(searchButton).toBe(null);
});
it('should not have animation', () => {
userPreferencesService.setWithoutStore('textOrientation', 'rtl');
fixture.detectChanges();
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();
await fixture.whenStable();
expect(element.querySelector('#adf-control-input').getAttribute('autocomplete')).toBe('on');
});
});
describe('Search visibility', () => {
beforeEach(() => {
userPreferencesService.setWithoutStore('textOrientation', 'ltr');
fixture.detectChanges();
});
it('should emit an event when the search becomes active', fakeAsync(() => {
const searchVisibilityChangeSpy = spyOn(component.searchVisibility, 'emit');
component.toggleSearchBar();
tick(200);
expect(searchVisibilityChangeSpy).toHaveBeenCalledWith(true);
}));
it('should emit an event when the search becomes inactive', fakeAsync(() => {
component.toggleSearchBar();
tick(200);
expect(component.subscriptAnimationState.value).toEqual('active');
const searchVisibilityChangeSpy = spyOn(component.searchVisibility, 'emit');
component.toggleSearchBar();
tick(200);
expect(component.subscriptAnimationState.value).toEqual('inactive');
expect(searchVisibilityChangeSpy).toHaveBeenCalledWith(false);
}));
it('should reset emit when the search becomes inactive', fakeAsync(() => {
const resetSpy = spyOn(component.reset, 'emit');
component.toggleSearchBar();
tick(200);
expect(component.subscriptAnimationState.value).toEqual('active');
component.searchTerm = 'fake-search-term';
component.toggleSearchBar();
tick(200);
expect(resetSpy).toHaveBeenCalled();
expect(component.searchTerm).toEqual('');
}));
describe('Clear button', () => {
beforeEach(fakeAsync(() => {
fixture.detectChanges();
component.subscriptAnimationState.value = 'active';
fixture.detectChanges();
tick(200);
}));
it('should clear button be visible when showClearButton is set to true', async () => {
component.showClearButton = true;
fixture.detectChanges();
await fixture.whenStable();
const clearButton = fixture.debugElement.query(By.css('[data-automation-id="adf-clear-search-button"]'));
expect(clearButton).not.toBeNull();
});
it('should clear button not be visible when showClearButton is set to false', () => {
component.showClearButton = false;
fixture.detectChanges();
const clearButton = fixture.debugElement.query(By.css('[data-automation-id="adf-clear-search-button"]'));
expect(clearButton).toBeNull();
});
it('should reset the search when clicking the clear button', async () => {
const resetEmitSpy = spyOn(component.reset, 'emit');
const searchVisibilityChangeSpy = spyOn(component.searchVisibility, 'emit');
component.searchTerm = 'fake-search-term';
component.showClearButton = true;
fixture.detectChanges();
await fixture.whenStable();
const clearButton = fixture.debugElement.query(By.css('[data-automation-id="adf-clear-search-button"]'));
clearButton.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
fixture.detectChanges();
await fixture.whenStable();
expect(resetEmitSpy).toHaveBeenCalled();
expect(searchVisibilityChangeSpy).toHaveBeenCalledWith(false);
expect(component.subscriptAnimationState.value).toEqual('inactive');
expect(component.searchTerm).toEqual('');
});
});
describe('Collapse on blur', () => {
beforeEach(fakeAsync(() => {
fixture.detectChanges();
component.toggleSearchBar();
tick(200);
}));
it('should collapse search on blur when the collapseOnBlur is set to true', fakeAsync (() => {
const searchVisibilityChangeSpy = spyOn(component.searchVisibility, 'emit');
const resetEmitSpy = spyOn(component.reset, 'emit');
component.collapseOnBlur = true;
component.searchTerm = 'fake-search-term';
component.onBlur({ relatedTarget: null });
tick(200);
expect(searchVisibilityChangeSpy).toHaveBeenCalledWith(false);
expect(component.subscriptAnimationState.value).toEqual('inactive');
expect(component.searchTerm).toEqual('');
expect(resetEmitSpy).toHaveBeenCalled();
}));
it('should not collapse search on blur when the collapseOnBlur is set to false', () => {
const searchVisibilityChangeSpy = spyOn(component.searchVisibility, 'emit');
component.searchTerm = 'fake-search-term';
component.collapseOnBlur = false;
component.onBlur({ relatedTarget: null });
expect(searchVisibilityChangeSpy).not.toHaveBeenCalled();
expect(component.subscriptAnimationState.value).toEqual('active');
expect(component.searchTerm).toEqual('fake-search-term');
});
});
});
});

View File

@@ -1,323 +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, 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 '@alfresco/adf-core';
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;
/** Trigger autocomplete results on input change. */
@Input()
searchAutocomplete: any = false;
/** Search term preselected */
@Input()
searchTerm: string = '';
/** Debounce time in milliseconds. */
@Input()
debounceTime: number = 0;
/** Listener for results-list events (focus, blur and focusout). */
@Input()
focusListener: Observable<FocusEvent>;
/** Collapse search bar on submit. */
@Input()
collapseOnSubmit: boolean = true;
/** Default state expanded or Collapsed. */
@Input()
defaultState: SearchTextStateEnum = SearchTextStateEnum.collapsed;
/** Toggles whether to collapse the search on blur. */
@Input()
collapseOnBlur: boolean = true;
/** Toggles whether to show a clear button that closes the search */
@Input()
showClearButton: boolean = false;
/** Placeholder text to show in the input field */
@Input()
placeholder: string = '';
/** Hint label */
@Input()
hintLabel = '';
/** 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();
/** Emitted when the result list is selected */
@Output()
selectResult: EventEmitter<any> = new EventEmitter();
/** Emitted when the result list is reset */
@Output()
reset: EventEmitter<boolean> = new EventEmitter();
/** Emitted when the search visibility changes. True when the search is active, false when it is inactive */
@Output()
searchVisibility: EventEmitter<boolean> = new EventEmitter<boolean>();
@ViewChild('searchInput', { static: true })
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();
}
}
this.emitVisibilitySearch();
}
});
}
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.isDefaultStateCollapsed()) {
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.isDefaultStateExpanded()) {
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) => 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 (this.collapseOnBlur && !$event.relatedTarget) {
this.resetSearch();
}
}
inputChange($event: any) {
this.valueChange.next($event);
}
toggleSearchBar() {
if (this.toggleSearch) {
this.toggleSearch.next();
}
}
searchSubmit(event: any) {
this.submit.emit(event);
if (this.collapseOnSubmit) {
this.toggleSearchBar();
}
}
activateToolbar(): boolean {
if (!this.isSearchBarActive()) {
this.toggleSearchBar();
}
return false;
}
isSearchBarActive(): boolean {
return this.subscriptAnimationState.value === 'active';
}
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();
}
canShowClearSearch(): boolean {
return this.showClearButton && this.isSearchBarActive();
}
resetSearch() {
if (this.isSearchBarActive()) {
this.toggleSearchBar();
}
}
private isDefaultStateCollapsed(): boolean {
return this.defaultState === SearchTextStateEnum.collapsed;
}
private isDefaultStateExpanded(): boolean {
return this.defaultState === SearchTextStateEnum.expanded;
}
private emitVisibilitySearch() {
this.searchVisibility.emit(this.isSearchBarActive());
}
}

View File

@@ -1,42 +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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { SearchTextInputComponent } from './search-text-input.component';
import { TranslateModule } from '@ngx-translate/core';
import { SearchTriggerDirective } from './search-trigger.directive';
import { MaterialModule } from '../material.module';
@NgModule({
declarations: [
SearchTextInputComponent,
SearchTriggerDirective
],
imports: [
CommonModule,
TranslateModule,
MaterialModule,
FormsModule
],
exports: [
SearchTextInputComponent,
SearchTriggerDirective
]
})
export class SearchTextModule {}

View File

@@ -1,222 +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.
*/
/* eslint-disable @angular-eslint/no-input-rename, @typescript-eslint/no-use-before-define, @angular-eslint/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 '../common/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({
// eslint-disable-next-line @angular-eslint/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) => any): void {
this.onChange = fn;
}
registerOnTouched(fn: () => any) {
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();
}
}

View File

@@ -20,6 +20,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import {
AuthenticationService,
SearchTextInputComponent,
setupTestBed,
UserPreferencesService
} from '@alfresco/adf-core';
@@ -29,7 +30,6 @@ import { SearchService } from '../services/search.service';
import { of } from 'rxjs';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { SearchTextInputComponent } from '../../search-text/search-text-input.component';
@Component({
template: `

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { AuthenticationService, ThumbnailService } from '@alfresco/adf-core';
import { AuthenticationService, ThumbnailService, SearchTextInputComponent } from '@alfresco/adf-core';
import { Component, EventEmitter, Input, OnDestroy, Output,
QueryList, ViewEncapsulation, ViewChild, ViewChildren, TemplateRef, ContentChild } from '@angular/core';
import { NodeEntry } from '@alfresco/js-api';
@@ -23,7 +23,6 @@ import { Subject } from 'rxjs';
import { SearchComponent } from './search.component';
import { MatListItem } from '@angular/material/list';
import { EmptySearchResultComponent } from './empty-search-result.component';
import { SearchTextInputComponent } from '../../search-text/search-text-input.component';
@Component({
selector: 'adf-search-control',

View File

@@ -16,7 +16,6 @@
*/
import { SearchService } from '../services/search.service';
import { SearchComponentInterface } from '../../common/interface/search-configuration.interface';
import {
AfterContentInit,
Component,
@@ -34,6 +33,7 @@ import {
import { NodePaging, ResultSetPaging } from '@alfresco/js-api';
import { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { SearchComponentInterface } from '@alfresco/adf-core';
@Component({
selector: 'adf-search',

View File

@@ -20,7 +20,7 @@ import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MaterialModule } from '../material.module';
import { CoreModule } from '@alfresco/adf-core';
import { CoreModule, SearchTextModule } from '@alfresco/adf-core';
import { SearchControlComponent } from './components/search-control.component';
import { SearchComponent } from './components/search.component';
@@ -47,7 +47,6 @@ import { SearchFacetFieldComponent } from './components/search-facet-field/searc
import { SearchWidgetChipComponent } from './components/search-filter-chips/search-widget-chip/search-widget-chip.component';
import { SearchFacetChipComponent } from './components/search-filter-chips/search-facet-chip/search-facet-chip.component';
import { ResetSearchDirective } from './components/reset-search.directive';
import { SearchTextModule } from '../search-text/search-text-input.module';
@NgModule({
imports: [

View File

@@ -17,7 +17,7 @@
import { Injectable } from '@angular/core';
import { QueryBody } from '@alfresco/js-api';
import { SearchConfigurationInterface } from '../../common/interface/search-configuration.interface';
import { SearchConfigurationInterface } from '../../common/interfaces/search-configuration.interface';
@Injectable({
providedIn: 'root'

View File

@@ -43,7 +43,6 @@ export * from './lib/pipes/index';
export * from './lib/common/index';
export * from './lib/tree/index';
export * from './lib/category/index';
export * from './lib/search-text/index';
export * from './lib/viewer/index';
export * from './lib/content.module';