mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-31 17:38:48 +00:00
@@ -0,0 +1,64 @@
|
||||
<div class="adf-search-container" *ngIf="isLoggedIn()"
|
||||
[@transitionMessages]="subscriptAnimationState">
|
||||
<a mat-icon-button
|
||||
*ngIf="expandable"
|
||||
id="adf-search-button"
|
||||
class="adf-search-button"
|
||||
(click)="toggleSearchBar($event)"
|
||||
(keyup.enter)="toggleSearchBar($event)">
|
||||
<mat-icon aria-label="search button">search</mat-icon>
|
||||
</a>
|
||||
<mat-form-field class="adf-input-form-field-divider">
|
||||
<input matInput
|
||||
[type]="inputType"
|
||||
[autocomplete]="getAutoComplete()"
|
||||
id="adf-control-input"
|
||||
[(ngModel)]="searchTerm"
|
||||
(focus)="activateToolbar()"
|
||||
(blur)="onBlur($event)"
|
||||
(keyup.escape)="toggleSearchBar()"
|
||||
(ngModelChange)="inputChange($event)"
|
||||
[searchAutocomplete]="auto"
|
||||
(keyup.enter)="searchSubmit($event)">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<adf-search #auto="searchAutocomplete"
|
||||
class="adf-search-result-autocomplete"
|
||||
[rootNodeId]="liveSearchRoot"
|
||||
[resultType]="liveSearchResultType"
|
||||
[resultSort]="liveSearchResultSort"
|
||||
[maxResults]="liveSearchMaxResults">
|
||||
<ng-template let-data>
|
||||
<mat-list *ngIf="isSearchBarActive()" id="autocomplete-search-result-list">
|
||||
<mat-list-item
|
||||
*ngFor="let item of data?.list?.entries; let idx = index"
|
||||
id="result_option_{{idx}}"
|
||||
[tabindex]="0"
|
||||
(focus)="onFocus($event)"
|
||||
(blur)="onBlur($event)"
|
||||
class="adf-search-autocomplete-item"
|
||||
(click)="elementClicked(item)"
|
||||
(keyup.enter)="elementClicked(item)">
|
||||
<mat-icon mat-list-icon>
|
||||
<img [src]="getMimeTypeIcon(item)" />
|
||||
</mat-icon>
|
||||
<h4 mat-line id="result_name_{{idx}}"
|
||||
*ngIf="highlight; else elseBlock"
|
||||
class="adf-search-fixed-text"
|
||||
[innerHtml]="item.entry.name | highlight: searchTerm">
|
||||
{{ item?.entry.name }}</h4>
|
||||
<ng-template #elseBlock>
|
||||
<h4 class="adf-search-fixed-text" mat-line id="result_name_{{idx}}" [innerHtml]="item.entry.name"></h4>
|
||||
</ng-template>
|
||||
<p mat-line class="adf-search-fixed-text"> {{item?.entry.createdByUser.displayName}} </p>
|
||||
</mat-list-item>
|
||||
<mat-list-item
|
||||
id="search_no_result"
|
||||
data-automation-id="search_no_result_found"
|
||||
*ngIf="data?.list?.entries.length === 0">
|
||||
<p mat-line class="adf-search-fixed-text">{{ 'SEARCH.RESULTS.NONE' | translate:{searchTerm: searchTerm} }}</p>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</ng-template>
|
||||
</adf-search>
|
@@ -0,0 +1,58 @@
|
||||
@mixin adf-search-control-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;
|
||||
|
||||
.adf {
|
||||
|
||||
&-search-fixed-text {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&-input-form-field-divider {
|
||||
.mat-form-field-underline {
|
||||
background-color: mat-color($primary, 50);
|
||||
.mat-form-field-ripple {
|
||||
background-color: mat-color($primary, 50);
|
||||
}
|
||||
}
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&-search-result-autocomplete {
|
||||
@include mat-menu-base(2);
|
||||
transform-origin: top left;
|
||||
position: absolute;
|
||||
max-width: 200px;
|
||||
max-height: 400px;
|
||||
margin-left: 45px;
|
||||
margin-top: -22px;
|
||||
font-size: 15px;
|
||||
z-index: 5;
|
||||
color: mat-color($foreground, text);
|
||||
background-color: mat-color($background, card);
|
||||
border-radius: $mat-menu-border-radius;
|
||||
|
||||
|
||||
@media screen and ($mat-small) {
|
||||
width: 160px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&-search-autocomplete-item {
|
||||
&:hover {
|
||||
background-color: mat-color($background, 'hover');
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: mat-color($primary, 900);
|
||||
}
|
||||
}
|
@@ -0,0 +1,425 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { DebugElement } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MaterialModule } from '../../material.module';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { AuthenticationService, SearchService } from '@alfresco/core';
|
||||
import { ThumbnailService } from '@alfresco/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { noResult, results } from '../../mock';
|
||||
import { SearchControlComponent } from './search-control.component';
|
||||
import { SearchTriggerDirective } from './search-trigger.directive';
|
||||
import { SearchComponent } from './search.component';
|
||||
|
||||
xdescribe('SearchControlComponent', () => {
|
||||
|
||||
let fixture: ComponentFixture<SearchControlComponent>;
|
||||
let component: SearchControlComponent;
|
||||
let element: HTMLElement;
|
||||
let debugElement: DebugElement;
|
||||
let searchService: SearchService;
|
||||
let authService: AuthenticationService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MaterialModule
|
||||
],
|
||||
declarations: [
|
||||
SearchControlComponent,
|
||||
SearchComponent,
|
||||
SearchTriggerDirective
|
||||
],
|
||||
providers: [
|
||||
ThumbnailService,
|
||||
SearchService
|
||||
]
|
||||
}).compileComponents().then(() => {
|
||||
fixture = TestBed.createComponent(SearchControlComponent);
|
||||
debugElement = fixture.debugElement;
|
||||
searchService = TestBed.get(SearchService);
|
||||
authService = TestBed.get(AuthenticationService);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.nativeElement;
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
spyOn(authService, 'isLoggedIn').and.returnValue(true);
|
||||
}));
|
||||
|
||||
afterEach(async(() => {
|
||||
fixture.destroy();
|
||||
TestBed.resetTestingModule();
|
||||
}));
|
||||
|
||||
describe('when input values are inserted', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should emit searchChange when search term input changed', async(() => {
|
||||
spyOn(searchService, 'getNodeQueryResults').and.callFake(() => {
|
||||
return Observable.of({ entry: { list: []}});
|
||||
});
|
||||
component.searchChange.subscribe(value => {
|
||||
expect(value).toBe('customSearchTerm');
|
||||
});
|
||||
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'customSearchTerm';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should update FAYT search when user inputs a valid term', async(() => {
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'customSearchTerm';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(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();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should NOT update FAYT term when user inputs a search term less than 3 characters', async(() => {
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'cu';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('#result_option_0')).toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should still fire an event when user inputs a search term less than 3 characters', async(() => {
|
||||
component.searchChange.subscribe(value => {
|
||||
expect(value).toBe('cu');
|
||||
});
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'cu';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('expandable option false', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
component.expandable = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('search button should be hide', () => {
|
||||
let searchButton: any = element.querySelector('#adf-search-button');
|
||||
expect(searchButton).toBe(null);
|
||||
});
|
||||
|
||||
it('should not have animation', () => {
|
||||
expect(component.subscriptAnimationState).toBe('no-animation');
|
||||
});
|
||||
});
|
||||
|
||||
describe('component rendering', () => {
|
||||
|
||||
it('should display a text input field by default', async(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelectorAll('input[type="text"]').length).toBe(1);
|
||||
expect(element.querySelector('#adf-control-input')).toBeDefined();
|
||||
expect(element.querySelector('#adf-control-input')).not.toBeNull();
|
||||
}));
|
||||
|
||||
it('should set browser autocomplete to off by default', async(() => {
|
||||
fixture.detectChanges();
|
||||
let attr = element.querySelectorAll('input[type="text"]')[0].getAttribute('autocomplete');
|
||||
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.querySelectorAll('input[type="text"]')[0].getAttribute('autocomplete')).toBe('on');
|
||||
}));
|
||||
|
||||
it('should fire a search when a enter key is pressed', async(() => {
|
||||
component.submit.subscribe((value) => {
|
||||
expect(value).toBe('TEST');
|
||||
});
|
||||
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
|
||||
fixture.detectChanges();
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'TEST';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
let enterKeyEvent: any = new Event('keyup');
|
||||
enterKeyEvent.keyCode = '13';
|
||||
inputDebugElement.nativeElement.dispatchEvent(enterKeyEvent);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('autocomplete list', () => {
|
||||
|
||||
it('should make autocomplete list control hidden initially', async(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('#autocomplete-search-result-list')).toBeNull();
|
||||
}));
|
||||
|
||||
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);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
fixture.detectChanges();
|
||||
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'TEST';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let resultElement: Element = element.querySelector('#autocomplete-search-result-list');
|
||||
expect(resultElement).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show autocomplete list noe results when search box has focus and there is search result with length 0', async(() => {
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(noResult));
|
||||
fixture.detectChanges();
|
||||
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'NO RES';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let noResultElement: Element = element.querySelector('#search_no_result');
|
||||
expect(noResultElement).not.toBe(null);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should hide autocomplete list results when the search box loses focus', (done) => {
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
fixture.detectChanges();
|
||||
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'NO RES';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let resultElement: Element = element.querySelector('#autocomplete-search-result-list');
|
||||
expect(resultElement).not.toBe(null);
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('blur'));
|
||||
|
||||
fixture.detectChanges();
|
||||
resultElement = element.querySelector('#autocomplete-search-result-list');
|
||||
expect(resultElement).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep autocomplete list control visible when user tabs into results', async(() => {
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
fixture.detectChanges();
|
||||
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'TEST';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let resultElement: HTMLElement = <HTMLElement> element.querySelector('#result_option_0');
|
||||
resultElement.focus();
|
||||
expect(resultElement).not.toBe(null);
|
||||
inputDebugElement.nativeElement.dispatchEvent(new KeyboardEvent('keypress', { key: 'TAB' }));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.querySelector('#autocomplete-search-result-list') ).not.toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should focus input element when autocomplete list is cancelled', async(() => {
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
fixture.detectChanges();
|
||||
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
let escapeEvent: any = new Event('ESCAPE');
|
||||
escapeEvent.keyCode = 27;
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(escapeEvent);
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(element.querySelector('#result_name_0') ).toBeNull();
|
||||
expect(document.activeElement.id).toBe(inputDebugElement.nativeElement.id);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should NOT display a autocomplete list control when configured not to', async(() => {
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
component.liveSearchEnabled = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'TEST';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('#autocomplete-search-result-list') ).toBeNull();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('search button', () => {
|
||||
|
||||
it('should NOT display a autocomplete list control when configured not to', async(() => {
|
||||
component.subscriptAnimationState = 'active';
|
||||
fixture.detectChanges();
|
||||
|
||||
let searchButton: DebugElement = fixture.debugElement.query(By.css('#adf-search-button'));
|
||||
searchButton.triggerEventHandler('click', null);
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.subscriptAnimationState).toBe('inactive');
|
||||
});
|
||||
}));
|
||||
|
||||
it('click on the search button should open the input box when is close', (done) => {
|
||||
component.subscriptAnimationState = 'inactive';
|
||||
fixture.detectChanges();
|
||||
let searchButton: DebugElement = fixture.debugElement.query(By.css('#adf-search-button'));
|
||||
searchButton.triggerEventHandler('click', null);
|
||||
window.setTimeout(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.subscriptAnimationState).toBe('active');
|
||||
done();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
it('Search button should not change the input state too often', async(() => {
|
||||
component.subscriptAnimationState = 'active';
|
||||
fixture.detectChanges();
|
||||
let searchButton: DebugElement = fixture.debugElement.query(By.css('#adf-search-button'));
|
||||
searchButton.triggerEventHandler('click', null);
|
||||
fixture.detectChanges();
|
||||
searchButton.triggerEventHandler('click', null);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.subscriptAnimationState).toBe('inactive');
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('option click', () => {
|
||||
|
||||
it('should emit a option clicked event when item is clicked', async(() => {
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
component.optionClicked.subscribe((item) => {
|
||||
expect(item.entry.id).toBe('123');
|
||||
});
|
||||
fixture.detectChanges();
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'TEST';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let firstOption: DebugElement = fixture.debugElement.query(By.css('#result_name_0'));
|
||||
firstOption.triggerEventHandler('click', null);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should set deactivate the search after element is clicked', async(() => {
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
component.optionClicked.subscribe((item) => {
|
||||
window.setTimeout(() => {
|
||||
expect(component.subscriptAnimationState).toBe('inactive');
|
||||
}, 200);
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'TEST';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let firstOption: DebugElement = fixture.debugElement.query(By.css('#result_name_0'));
|
||||
firstOption.triggerEventHandler('click', null);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should NOT reset the search term after element is clicked', async(() => {
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
component.optionClicked.subscribe((item) => {
|
||||
expect(component.searchTerm).not.toBeFalsy();
|
||||
expect(component.searchTerm).toBe('TEST');
|
||||
});
|
||||
fixture.detectChanges();
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'TEST';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let firstOption: DebugElement = fixture.debugElement.query(By.css('#result_name_0'));
|
||||
firstOption.triggerEventHandler('click', null);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
@@ -0,0 +1,178 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { AuthenticationService, ThumbnailService } from '@alfresco/core';
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-control',
|
||||
templateUrl: './search-control.component.html',
|
||||
styleUrls: ['./search-control.component.scss'],
|
||||
animations: [
|
||||
trigger('transitionMessages', [
|
||||
state('active', style({transform: 'translateX(0%)'})),
|
||||
state('inactive', style({transform: 'translateX(83%)', overflow: 'hidden'})),
|
||||
state('no-animation', style({transform: 'translateX(0%)', width: '100%'})),
|
||||
transition('inactive => active',
|
||||
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')),
|
||||
transition('active => inactive',
|
||||
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)'))
|
||||
])
|
||||
]
|
||||
})
|
||||
export class SearchControlComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input()
|
||||
expandable: boolean = true;
|
||||
|
||||
@Input()
|
||||
highlight: boolean = false;
|
||||
|
||||
@Input()
|
||||
inputType: string = 'text';
|
||||
|
||||
@Input()
|
||||
autocomplete: boolean = false;
|
||||
|
||||
@Input()
|
||||
liveSearchEnabled: boolean = true;
|
||||
|
||||
@Input()
|
||||
liveSearchRoot: string = '-root-';
|
||||
|
||||
@Input()
|
||||
liveSearchResultType: string = null;
|
||||
|
||||
@Input()
|
||||
liveSearchResultSort: string = null;
|
||||
|
||||
@Input()
|
||||
liveSearchMaxResults: number = 5;
|
||||
|
||||
@Output()
|
||||
submit: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
searchChange: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
optionClicked: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
searchTerm: string = '';
|
||||
subscriptAnimationState: string;
|
||||
|
||||
private toggleSearch = new Subject<any>();
|
||||
private focusSubject = new Subject<FocusEvent>();
|
||||
|
||||
constructor(public authService: AuthenticationService,
|
||||
private thumbnailService: ThumbnailService) {
|
||||
|
||||
this.toggleSearch.asObservable().debounceTime(100).subscribe(() => {
|
||||
if (this.expandable) {
|
||||
this.subscriptAnimationState = this.subscriptAnimationState === 'inactive' ? 'active' : 'inactive';
|
||||
|
||||
if (this.subscriptAnimationState === 'inactive') {
|
||||
this.searchTerm = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.subscriptAnimationState = this.expandable ? 'inactive' : 'no-animation';
|
||||
this.setupFocusEventHandlers();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.focusSubject.unsubscribe();
|
||||
this.toggleSearch.unsubscribe();
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return this.authService.isLoggedIn();
|
||||
}
|
||||
|
||||
searchSubmit(event: any) {
|
||||
this.submit.emit(event);
|
||||
this.toggleSearchBar();
|
||||
}
|
||||
|
||||
inputChange(event: any) {
|
||||
this.searchChange.emit(event);
|
||||
}
|
||||
|
||||
getAutoComplete(): string {
|
||||
return this.autocomplete ? 'on' : 'off';
|
||||
}
|
||||
|
||||
getMimeTypeIcon(node: MinimalNodeEntity): string {
|
||||
let mimeType;
|
||||
|
||||
if (node.entry.content && node.entry.content.mimeType) {
|
||||
mimeType = node.entry.content.mimeType;
|
||||
}
|
||||
if (node.entry.isFolder) {
|
||||
mimeType = 'folder';
|
||||
}
|
||||
|
||||
return this.thumbnailService.getMimeTypeIcon(mimeType);
|
||||
}
|
||||
|
||||
isSearchBarActive() {
|
||||
return this.subscriptAnimationState === 'active' && this.liveSearchEnabled;
|
||||
}
|
||||
|
||||
toggleSearchBar() {
|
||||
this.toggleSearch.next();
|
||||
}
|
||||
|
||||
elementClicked(item: any) {
|
||||
if (item.entry) {
|
||||
this.optionClicked.next(item);
|
||||
this.toggleSearchBar();
|
||||
}
|
||||
}
|
||||
|
||||
onFocus($event): void {
|
||||
this.focusSubject.next($event);
|
||||
}
|
||||
|
||||
onBlur($event): void {
|
||||
this.focusSubject.next($event);
|
||||
}
|
||||
|
||||
activateToolbar($event) {
|
||||
if ( !this.isSearchBarActive() ) {
|
||||
this.toggleSearchBar();
|
||||
}
|
||||
}
|
||||
|
||||
private setupFocusEventHandlers() {
|
||||
let focusEvents: Observable<FocusEvent> = this.focusSubject.asObservable()
|
||||
.distinctUntilChanged().debounceTime(50);
|
||||
focusEvents.filter(($event: any) => {
|
||||
return this.isSearchBarActive() && ($event.type === 'blur' || $event.type === 'focusout');
|
||||
}).subscribe(() => {
|
||||
this.toggleSearchBar();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,215 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { DOWN_ARROW, ENTER, ESCAPE, UP_ARROW } 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/platform-browser';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { fromEvent } from 'rxjs/observable/fromEvent';
|
||||
import { merge } from 'rxjs/observable/merge';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { SearchComponent } from './search.component';
|
||||
|
||||
export const AUTOCOMPLETE_OPTION_HEIGHT = 48;
|
||||
|
||||
export const AUTOCOMPLETE_PANEL_HEIGHT = 256;
|
||||
|
||||
export const SEARCH_AUTOCOMPLETE_VALUE_ACCESSOR: any = {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => SearchTriggerDirective),
|
||||
multi: true
|
||||
};
|
||||
|
||||
const MIN_WORD_LENGTH_VALID = 3;
|
||||
|
||||
@Directive({
|
||||
selector: `input[searchAutocomplete], textarea[searchAutocomplete]`,
|
||||
host: {
|
||||
'role': 'combobox',
|
||||
'autocomplete': 'off',
|
||||
'aria-autocomplete': 'list',
|
||||
'[attr.aria-expanded]': 'panelOpen.toString()',
|
||||
'[attr.aria-owns]': 'autocomplete?.id',
|
||||
'(blur)': 'onTouched()',
|
||||
'(input)': 'handleInput($event)',
|
||||
'(keydown)': 'handleKeydown($event)'
|
||||
},
|
||||
providers: [SEARCH_AUTOCOMPLETE_VALUE_ACCESSOR]
|
||||
})
|
||||
export class SearchTriggerDirective implements ControlValueAccessor, OnDestroy {
|
||||
|
||||
@Input('searchAutocomplete')
|
||||
searchPanel: SearchComponent;
|
||||
|
||||
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.escapeEventStream.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 Observable.of(null);
|
||||
}
|
||||
|
||||
return merge(
|
||||
fromEvent(this.document, 'click'),
|
||||
fromEvent(this.document, 'touchend')
|
||||
).filter((event: MouseEvent | TouchEvent) => {
|
||||
const clickTarget = event.target as HTMLElement;
|
||||
return this._panelOpen &&
|
||||
clickTarget !== this.element.nativeElement;
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}else {
|
||||
let isArrowKey = keyCode === UP_ARROW || keyCode === DOWN_ARROW;
|
||||
if ( isArrowKey ) {
|
||||
if ( !this.panelOpen ) {
|
||||
this.openPanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleInput(event: KeyboardEvent): void {
|
||||
if (document.activeElement === event.target) {
|
||||
let inputValue: string = (event.target as HTMLInputElement).value;
|
||||
this.onChange(inputValue);
|
||||
if (inputValue.length >= MIN_WORD_LENGTH_VALID) {
|
||||
this.searchPanel.keyPressedStream.next(inputValue);
|
||||
this.openPanel();
|
||||
} else {
|
||||
this.searchPanel.resetResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isPanelOptionClicked(event: MouseEvent) {
|
||||
let isPanelOption: boolean = false;
|
||||
if ( event ) {
|
||||
let clickTarget = event.target as HTMLElement;
|
||||
isPanelOption = !this.isNoResultOption(event) &&
|
||||
!!this.searchPanel.panel &&
|
||||
!!this.searchPanel.panel.nativeElement.contains(clickTarget);
|
||||
}
|
||||
return isPanelOption;
|
||||
}
|
||||
|
||||
private isNoResultOption(event: MouseEvent) {
|
||||
return 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)
|
||||
.switchMap(() => {
|
||||
this.searchPanel.setVisibility();
|
||||
return this.panelClosingActions;
|
||||
})
|
||||
.first()
|
||||
.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)) {
|
||||
this.setTriggerValue(event.target.textContent.trim());
|
||||
this.onChange(event.target.textContent.trim());
|
||||
this.element.nativeElement.focus();
|
||||
}
|
||||
this.closePanel();
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
<div role="listbox" id="adf-search-results-content" [ngClass]="_classList" #panel>
|
||||
<ng-template
|
||||
[ngTemplateOutlet]="template"
|
||||
[ngTemplateOutletContext]="{ $implicit: results }">
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
|
19
lib/content-services/search/components/search.component.scss
Normal file
19
lib/content-services/search/components/search.component.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
@mixin adf-search-autocomplete-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$accent: map-get($theme, accent);
|
||||
$warn: map-get($theme, warn);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
$mat-menu-border-radius: 2px !default;
|
||||
|
||||
.adf {
|
||||
|
||||
&-search-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&-search-show {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
114
lib/content-services/search/components/search.component.spec.ts
Normal file
114
lib/content-services/search/components/search.component.spec.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SearchService } from '@alfresco/core';
|
||||
import { SearchModule } from '../../index';
|
||||
import { differentResult, result, SimpleSearchTestComponent } from '../../mock';
|
||||
|
||||
describe('SearchComponent', () => {
|
||||
|
||||
let fixture: ComponentFixture<SimpleSearchTestComponent>, element: HTMLElement;
|
||||
let component: SimpleSearchTestComponent;
|
||||
let searchService: SearchService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SearchModule
|
||||
],
|
||||
declarations: [ SimpleSearchTestComponent ]
|
||||
}).compileComponents().then(() => {
|
||||
fixture = TestBed.createComponent(SimpleSearchTestComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.nativeElement;
|
||||
searchService = TestBed.get(SearchService);
|
||||
});
|
||||
}));
|
||||
|
||||
describe('search results', () => {
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should clear results straight away when a new search term is entered', async(() => {
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValues(Promise.resolve(result), Promise.resolve(differentResult));
|
||||
|
||||
component.setSearchWordTo('searchTerm');
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let optionShowed = element.querySelectorAll('#autocomplete-search-result-list > li').length;
|
||||
expect(optionShowed).toBe(1);
|
||||
component.setSearchWordTo('searchTerm2');
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
optionShowed = element.querySelectorAll('#autocomplete-search-result-list > li').length;
|
||||
expect(optionShowed).toBe(1);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display the returned search results', async(() => {
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(result));
|
||||
|
||||
component.setSearchWordTo('searchTerm');
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('#result_option_0').textContent.trim()).toBe('MyDoc');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should emit error event when search call fail', async(() => {
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.reject({ status: 402 }));
|
||||
component.setSearchWordTo('searchTerm');
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let message: HTMLElement = <HTMLElement> element.querySelector('#component-result-message');
|
||||
expect(message.textContent).toBe('ERROR');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should be able to hide the result panel', async(() => {
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValues(Promise.resolve(result), Promise.resolve(differentResult));
|
||||
|
||||
component.setSearchWordTo('searchTerm');
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let optionShowed = element.querySelectorAll('#autocomplete-search-result-list');
|
||||
expect(optionShowed).not.toBeNull();
|
||||
component.forceHidePanel();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let elementList = element.querySelector('#adf-search-results-content');
|
||||
expect(elementList.classList).toContain('adf-search-hide');
|
||||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
186
lib/content-services/search/components/search.component.ts
Normal file
186
lib/content-services/search/components/search.component.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { SearchOptions, SearchService } from '@alfresco/core';
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { NodePaging } from 'alfresco-js-api';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search',
|
||||
templateUrl: './search.component.html',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
preserveWhitespaces: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
exportAs: 'searchAutocomplete',
|
||||
host: {
|
||||
'class': 'adf-search'
|
||||
}
|
||||
})
|
||||
export class SearchComponent implements AfterContentInit, OnChanges {
|
||||
|
||||
@ViewChild('panel')
|
||||
panel: ElementRef;
|
||||
|
||||
@ContentChild(TemplateRef)
|
||||
template: TemplateRef<any>;
|
||||
|
||||
@Input()
|
||||
displayWith: ((value: any) => string) | null = null;
|
||||
|
||||
@Input()
|
||||
maxResults: number = 20;
|
||||
|
||||
@Input()
|
||||
resultSort: string = null;
|
||||
|
||||
@Input()
|
||||
rootNodeId: string = '-root-';
|
||||
|
||||
@Input()
|
||||
resultType: string = null;
|
||||
|
||||
@Input()
|
||||
searchTerm: string = '';
|
||||
|
||||
@Input('class')
|
||||
set classList(classList: string) {
|
||||
if (classList && classList.length) {
|
||||
classList.split(' ').forEach(className => this._classList[className.trim()] = true);
|
||||
this._elementRef.nativeElement.className = '';
|
||||
}
|
||||
}
|
||||
|
||||
@Output()
|
||||
resultLoaded: EventEmitter<NodePaging> = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
error: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
showPanel: boolean = false;
|
||||
results: NodePaging;
|
||||
|
||||
get isOpen(): boolean {
|
||||
return this._isOpen && this.showPanel;
|
||||
}
|
||||
|
||||
set isOpen(value: boolean) {
|
||||
this._isOpen = value;
|
||||
}
|
||||
|
||||
_isOpen: boolean = false;
|
||||
|
||||
keyPressedStream: Subject<string> = new Subject();
|
||||
|
||||
_classList: { [key: string]: boolean } = {};
|
||||
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private _elementRef: ElementRef) {
|
||||
this.keyPressedStream.asObservable()
|
||||
.debounceTime(200)
|
||||
.subscribe((searchedWord: string) => {
|
||||
this.displaySearchResults(searchedWord);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.setVisibility();
|
||||
}
|
||||
|
||||
ngOnChanges(changes) {
|
||||
if (changes.searchTerm) {
|
||||
this.resetResults();
|
||||
this.displaySearchResults(changes.searchTerm.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
resetResults() {
|
||||
this.cleanResults();
|
||||
this.setVisibility();
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.displaySearchResults(this.searchTerm);
|
||||
}
|
||||
|
||||
private cleanResults() {
|
||||
if (this.results) {
|
||||
this.results = {};
|
||||
}
|
||||
}
|
||||
|
||||
private displaySearchResults(searchTerm) {
|
||||
let searchOpts: SearchOptions = {
|
||||
include: ['path', 'allowableOperations'],
|
||||
rootNodeId: this.rootNodeId,
|
||||
nodeType: this.resultType,
|
||||
maxItems: this.maxResults,
|
||||
orderBy: this.resultSort
|
||||
};
|
||||
if (searchTerm !== null && searchTerm !== '') {
|
||||
searchTerm = searchTerm + '*';
|
||||
this.searchService
|
||||
.getNodeQueryResults(searchTerm, searchOpts)
|
||||
.subscribe(
|
||||
results => {
|
||||
this.results = <NodePaging> results;
|
||||
this.resultLoaded.emit(this.results);
|
||||
this.isOpen = true;
|
||||
this.setVisibility();
|
||||
},
|
||||
error => {
|
||||
if (error.status !== 400) {
|
||||
this.results = null;
|
||||
this.error.emit(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hidePanel() {
|
||||
if (this.isOpen) {
|
||||
this._classList['adf-search-show'] = false;
|
||||
this._classList['adf-search-hide'] = true;
|
||||
this.isOpen = false;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
setVisibility() {
|
||||
this.showPanel = !!this.results && !!this.results.list;
|
||||
this._classList['adf-search-show'] = this.showPanel;
|
||||
this._classList['adf-search-hide'] = !this.showPanel;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user