mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[ADF-1761] Search component refactoring (#2623)
* [ADF-1761] refactoring search * [ADF-1761] added click exit strategy and selection for list items * [ADF-1761] removed old search component replaced with the new implementation * [ADF-1761] removed old component and replaced with the refactored version * [ADF-1761] added the new tests for search control component * [ADF-1761] added test for refactored search component * [ADF-1761] fixed minor twitch start styling the list * [ADF-1761] fixed test * [ADF-1761] removed unused import * [ADF-1761] added search integrations with files component * [ADF-1761] rebased branch on lates development * [ADF-1761] added blur management for search * [ADF-1761] fixed wrong behaviour on blur * [ADF - 1761] fixed responsiveness on smaller resolution * [ADF-1761] reduced font and added white line * [ADF-1761] fixed some blur behaviour * [ADF-1761] added some fix for on blur behaviour * [ADF-1761] fixed some behaviour on blur * [ADF-1761] fix for angular 5 * [ADF-1761] changed name from search integration to search result into demoshell * [ADF-1761] fixed wrong change name * [ADF-1761] added documentation for search control component * [ADF-1761] added documentation for search component * [ADF-1761] fixed wrong link into documentation * [ADF-1761] fixed image for simple example * [ADF-1761] added some improvements and removed duplicated code * [ADF-1761] renamed directive to searchAutocomplete * [ADF-1761] added some changes after Peer review
This commit is contained in:
@@ -17,52 +17,36 @@
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatInputModule, MatListModule } from '@angular/material';
|
||||
import { CoreModule, SearchService, TRANSLATION_PROVIDER } from 'ng2-alfresco-core';
|
||||
import { DocumentListModule } from 'ng2-alfresco-documentlist';
|
||||
import { SearchAutocompleteComponent } from './src/components/search-autocomplete.component';
|
||||
import { SearchControlComponent } from './src/components/search-control.component';
|
||||
import { SearchTriggerDirective } from './src/components/search-trigger.directive';
|
||||
import { SearchComponent } from './src/components/search.component';
|
||||
|
||||
// services
|
||||
export { SearchOptions, SearchService } from 'ng2-alfresco-core';
|
||||
export * from './src/components/search.component';
|
||||
export * from './src/components/search-control.component';
|
||||
export * from './src/components/search-autocomplete.component';
|
||||
|
||||
// Old Deprecated export
|
||||
import { SearchService as AlfrescoSearchService } from 'ng2-alfresco-core';
|
||||
import { SearchAutocompleteComponent as AlfrescoSearchAutocompleteComponent } from './src/components/search-autocomplete.component';
|
||||
import { SearchControlComponent as AlfrescoSearchControlComponent } from './src/components/search-control.component';
|
||||
import { SearchComponent as AlfrescoSearchComponent } from './src/components/search.component';
|
||||
export { SearchService as AlfrescoSearchService } from 'ng2-alfresco-core';
|
||||
export { SearchComponent as AlfrescoSearchComponent } from './src/components/search.component';
|
||||
export { SearchControlComponent as AlfrescoSearchControlComponent } from './src/components/search-control.component';
|
||||
export { SearchAutocompleteComponent as AlfrescoSearchAutocompleteComponent } from './src/components/search-autocomplete.component';
|
||||
|
||||
export const ALFRESCO_SEARCH_DIRECTIVES: [any] = [
|
||||
SearchComponent,
|
||||
SearchControlComponent,
|
||||
SearchAutocompleteComponent,
|
||||
|
||||
// Old Deprecated export
|
||||
AlfrescoSearchComponent,
|
||||
AlfrescoSearchControlComponent,
|
||||
AlfrescoSearchAutocompleteComponent
|
||||
SearchTriggerDirective
|
||||
];
|
||||
|
||||
export const ALFRESCO_SEARCH_PROVIDERS: [any] = [
|
||||
SearchService,
|
||||
|
||||
// Old Deprecated export
|
||||
AlfrescoSearchService
|
||||
SearchService
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
DocumentListModule,
|
||||
CoreModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule
|
||||
ReactiveFormsModule,
|
||||
MatListModule,
|
||||
MatInputModule
|
||||
],
|
||||
declarations: [
|
||||
...ALFRESCO_SEARCH_DIRECTIVES
|
||||
|
@@ -15,6 +15,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { SearchComponent } from '../components/search.component';
|
||||
|
||||
const entryItem = {
|
||||
entry: {
|
||||
id: '123',
|
||||
@@ -32,6 +35,23 @@ const entryItem = {
|
||||
}
|
||||
};
|
||||
|
||||
const entryDifferentItem = {
|
||||
entry: {
|
||||
id: '999',
|
||||
name: 'TEST_DOC',
|
||||
isFile : true,
|
||||
content: {
|
||||
mimeType: 'text/plain'
|
||||
},
|
||||
createdByUser: {
|
||||
displayName: 'John TEST'
|
||||
},
|
||||
modifiedByUser: {
|
||||
displayName: 'John TEST'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export let result = {
|
||||
list: {
|
||||
entries: [
|
||||
@@ -40,6 +60,14 @@ export let result = {
|
||||
}
|
||||
};
|
||||
|
||||
export let differentResult = {
|
||||
list: {
|
||||
entries: [
|
||||
entryDifferentItem
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export let results = {
|
||||
list: {
|
||||
entries: [
|
||||
@@ -86,3 +114,56 @@ export let errorJson = {
|
||||
descriptionURL: 'https://api-explorer.alfresco.com'
|
||||
}
|
||||
};
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<adf-search [searchTerm]="searchedWord" [maxResults]="maxResults"
|
||||
(error)="showSearchResult('ERROR')"
|
||||
(success)="showSearchResult('success')" #search>
|
||||
<ng-template let-data>
|
||||
<ul id="autocomplete-search-result-list">
|
||||
<li *ngFor="let item of data?.list?.entries; let idx = index" (click)="elementClicked(item)">
|
||||
<div id="result_option_{{idx}}">
|
||||
<span>{{ item?.entry.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</adf-search>
|
||||
<span id="component-result-message">{{message}}</span>
|
||||
`
|
||||
})
|
||||
|
||||
export class SimpleSearchTestComponent {
|
||||
|
||||
@ViewChild('search')
|
||||
search: SearchComponent;
|
||||
|
||||
message: string = '';
|
||||
searchedWord= '';
|
||||
maxResults: number = 5;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
showSearchResult(event: any) {
|
||||
this.message = event;
|
||||
}
|
||||
|
||||
elementClicked(event: any) {
|
||||
this.message = 'element clicked';
|
||||
}
|
||||
|
||||
setSearchWordTo(str: string) {
|
||||
this.searchedWord = str;
|
||||
}
|
||||
|
||||
changeMaxResultTo(newMax: number) {
|
||||
this.maxResults = newMax;
|
||||
}
|
||||
|
||||
forceHidePanel() {
|
||||
this.search.hidePanel();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,40 +0,0 @@
|
||||
<table class="full-width adf-search-result"
|
||||
[@transformAutocomplete]="panelAnimationState"
|
||||
(@transformAutocomplete.done)="onAnimationDone($event)">
|
||||
<tbody id="adf-search-results" #resultsTableBody data-automation-id="autocomplete_results" *ngIf="results && results.length && searchTerm">
|
||||
<tr id="result_row_{{idx}}" *ngFor="let result of results; let idx = index" tabindex="0"
|
||||
(blur)="onRowBlur($event)" (focus)="onRowFocus($event)"
|
||||
(click)="onItemClick(result)"
|
||||
(keyup.enter)="onRowEnter(result)"
|
||||
(keyup.arrowdown)="onRowArrowDown($event)"
|
||||
(keyup.arrowup)="onRowArrowUp($event)"
|
||||
(keyup.escape)="onRowEscape($event)"
|
||||
attr.data-automation-id="autocomplete_result_for_{{result.entry.name}}">
|
||||
<td class="img-td"><img src="{{getMimeTypeIcon(result)}}" alt="{{result.entry.name}}"/></td>
|
||||
<td>
|
||||
<div id="result_name_{{idx}}" *ngIf="highlight; else elseBlock" class="truncate"
|
||||
[innerHtml]="result.entry.name | highlight: searchTerm"></div>
|
||||
<ng-template #elseBlock>
|
||||
<div id="result_name_{{idx}}" class="truncate" [innerHtml]="result.entry.name"></div>
|
||||
</ng-template>
|
||||
<div id="result_user_{{idx}}" class="truncate">{{result.entry.createdByUser.displayName}}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody id="search_no_result" data-automation-id="search_no_result_found" *ngIf="results && results.length === 0">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="truncate"><b> {{ 'SEARCH.RESULTS.NONE' | translate:{searchTerm: searchTerm} }}</b></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody data-automation-id="autocomplete_error_message" *ngIf="errorMessage">
|
||||
<tr>
|
||||
<td>{{ 'SEARCH.RESULTS.ERROR' | translate:{errorMessage: errorMessage} }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -1,78 +0,0 @@
|
||||
@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-result-container{
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&-search-result {
|
||||
@include mat-menu-base(2);
|
||||
transform-origin: top left;
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
color: mat-color($foreground, text);
|
||||
background-color: mat-color($background, card);
|
||||
margin: -21px 0 0 0;
|
||||
border-radius: $mat-menu-border-radius;
|
||||
border-collapse: collapse;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
|
||||
a {
|
||||
color: mat-color($foreground, text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
height: 32px;
|
||||
|
||||
&:hover {
|
||||
background-color: mat-color($background, hover);
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
height: 32px;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: mat-color($primary, 900);
|
||||
}
|
||||
|
||||
.img-td {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
width: 240px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
:host {
|
||||
right: 0;
|
||||
}
|
||||
.truncate {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,428 +0,0 @@
|
||||
/*!
|
||||
* @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 { ThumbnailService } from 'ng2-alfresco-core';
|
||||
import {
|
||||
AlfrescoApiService,
|
||||
AlfrescoAuthenticationService,
|
||||
AlfrescoContentService,
|
||||
AlfrescoSettingsService,
|
||||
AlfrescoTranslationService,
|
||||
CoreModule,
|
||||
SearchService
|
||||
} from 'ng2-alfresco-core';
|
||||
import { errorJson, folderResult, noResult, result, results } from './../assets/search.component.mock';
|
||||
import { TranslationMock } from './../assets/translation.service.mock';
|
||||
import { SearchAutocompleteComponent } from './search-autocomplete.component';
|
||||
|
||||
describe('SearchAutocompleteComponent', () => {
|
||||
|
||||
let fixture: ComponentFixture<SearchAutocompleteComponent>, element: HTMLElement;
|
||||
let component: SearchAutocompleteComponent;
|
||||
|
||||
let updateSearchTerm = (newSearchTerm: string): void => {
|
||||
let oldSearchTerm = component.searchTerm;
|
||||
component.searchTerm = newSearchTerm;
|
||||
component.ngOnChanges({searchTerm: { currentValue: newSearchTerm, previousValue: oldSearchTerm}});
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CoreModule
|
||||
],
|
||||
declarations: [ SearchAutocompleteComponent ], // declare the test component
|
||||
providers: [
|
||||
{provide: AlfrescoTranslationService, useClass: TranslationMock},
|
||||
ThumbnailService,
|
||||
AlfrescoSettingsService,
|
||||
AlfrescoApiService,
|
||||
AlfrescoAuthenticationService,
|
||||
AlfrescoContentService,
|
||||
SearchService
|
||||
]
|
||||
}).compileComponents().then(() => {
|
||||
fixture = TestBed.createComponent(SearchAutocompleteComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.nativeElement;
|
||||
});
|
||||
}));
|
||||
|
||||
describe('search results', () => {
|
||||
|
||||
let searchService;
|
||||
|
||||
beforeEach(() => {
|
||||
searchService = fixture.debugElement.injector.get(SearchService);
|
||||
});
|
||||
|
||||
it('should clear results straight away when a new search term is entered', async(() => {
|
||||
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(result));
|
||||
|
||||
component.searchTerm = 'searchTerm';
|
||||
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm', previousValue: ''} });
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
component.searchTerm = 'searchTerm2';
|
||||
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm2', previousValue: 'searchTerm'} });
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelectorAll('tbody[data-automation-id="autocomplete_results"] tr').length).toBe(0);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display the returned search results', (done) => {
|
||||
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(result));
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
expect( element.querySelector('#result_user_0').innerHTML).toBe('John Doe');
|
||||
expect( element.querySelector('#result_name_0').innerHTML).toContain('MyDoc');
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
it('should highlight the searched word', (done) => {
|
||||
component.highlight = true;
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(results));
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
let el: any = element.querySelectorAll('tbody[data-automation-id="autocomplete_results"] tr')[1].children[1].children[0];
|
||||
expect(el.innerText).toEqual('MyDoc');
|
||||
let spanHighlight = el.children[0];
|
||||
expect(spanHighlight.classList[0]).toEqual('highlight');
|
||||
expect(spanHighlight.innerText).toEqual('My');
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('My');
|
||||
|
||||
});
|
||||
|
||||
it('should limit the number of returned search results to the configured maximum', (done) => {
|
||||
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(results));
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelectorAll('tbody[data-automation-id="autocomplete_results"] tr').length).toBe(2);
|
||||
done();
|
||||
});
|
||||
|
||||
component.maxResults = 2;
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
it('should display the correct thumbnail for result items', (done) => {
|
||||
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(result));
|
||||
|
||||
let thumbnailService = fixture.debugElement.injector.get(ThumbnailService);
|
||||
spyOn(thumbnailService, 'getMimeTypeIcon').and.returnValue('fake-type-icon.svg');
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
let imgEl = <any> element.querySelector('#result_row_0 img');
|
||||
expect(imgEl).not.toBeNull();
|
||||
expect(imgEl.src).toContain('fake-type-icon.svg');
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
it('should display no result if no result are returned', (done) => {
|
||||
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(noResult));
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('#search_no_result')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
|
||||
let searchService;
|
||||
|
||||
beforeEach(() => {
|
||||
searchService = fixture.debugElement.injector.get(SearchService);
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.reject(errorJson));
|
||||
});
|
||||
|
||||
it('should display an error if an error is encountered running the search', (done) => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {}, () => {
|
||||
fixture.detectChanges();
|
||||
let resultsEl = element.querySelector('[data-automation-id="autocomplete_results"]');
|
||||
let errorEl = <any> element.querySelector('[data-automation-id="autocomplete_error_message"]');
|
||||
expect(resultsEl).toBeNull();
|
||||
expect(errorEl).not.toBeNull();
|
||||
expect(errorEl.innerText.trim()).toBe('SEARCH.RESULTS.ERROR');
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
it('should clear errors straight away when a new search is performed', async(() => {
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
component.searchTerm = 'searchTerm2';
|
||||
component.ngOnChanges({searchTerm: { currentValue: 'searchTerm2', previousValue: 'searchTerm'} });
|
||||
fixture.detectChanges();
|
||||
let errorEl = <any> element.querySelector('[data-automation-id="autocomplete_error_message"]');
|
||||
expect(errorEl).toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('mouse interactions', () => {
|
||||
|
||||
let searchService;
|
||||
|
||||
beforeEach(() => {
|
||||
searchService = fixture.debugElement.injector.get(SearchService);
|
||||
});
|
||||
|
||||
it('should emit fileSelect event when file item clicked', (done) => {
|
||||
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(result));
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
(<any> element.querySelector('#result_row_0')).click();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
|
||||
component.fileSelect.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit fileSelect event if when folder item clicked', (done) => {
|
||||
|
||||
spyOn(searchService, 'getQueryNodesPromise').and.returnValue(Promise.resolve(folderResult));
|
||||
|
||||
spyOn(component.fileSelect, 'emit');
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
(<any> element.querySelector('#result_row_0')).click();
|
||||
expect(component.fileSelect.emit).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('keyboard interactions', () => {
|
||||
|
||||
let searchService;
|
||||
|
||||
beforeEach(() => {
|
||||
searchService = fixture.debugElement.injector.get(SearchService);
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(results));
|
||||
});
|
||||
|
||||
it('should emit file select when enter key pressed when a file item is in focus', (done) => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
(<any> element.querySelector('#result_row_0')).dispatchEvent(new KeyboardEvent('keyup', {
|
||||
key: 'Enter'
|
||||
}));
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
|
||||
component.fileSelect.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit cancel event when escape key pressed when a result is in focus', (done) => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
(<any> element.querySelector('#result_row_0')).dispatchEvent(new KeyboardEvent('keyup', {
|
||||
key: 'Escape'
|
||||
}));
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
|
||||
component.cancel.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should focus the next result when down arrow key pressed when a result is in focus', (done) => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
let firstResult: any = element.querySelector('#result_row_0');
|
||||
let secondResult: any = element.querySelector('#result_row_1');
|
||||
spyOn(secondResult, 'focus');
|
||||
firstResult.focus();
|
||||
firstResult.dispatchEvent(new KeyboardEvent('keyup', {
|
||||
key: 'ArrowDown'
|
||||
}));
|
||||
expect(secondResult.focus).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
it('should do nothing when down arrow key pressed when the last result is in focus', (done) => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
let lastResult: any = element.querySelector('#result_row_2');
|
||||
lastResult.focus();
|
||||
lastResult.dispatchEvent(new KeyboardEvent('keyup', {
|
||||
key: 'ArrowDown'
|
||||
}));
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
it('should focus the previous result when up arrow key pressed when a result is in focus', (done) => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
let firstResult: any = element.querySelector('#result_row_0');
|
||||
let secondResult: any = element.querySelector('#result_row_1');
|
||||
spyOn(firstResult, 'focus');
|
||||
secondResult.focus();
|
||||
secondResult.dispatchEvent(new KeyboardEvent('keyup', {
|
||||
key: 'ArrowUp'
|
||||
}));
|
||||
expect(firstResult.focus).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
it('should emit scroll back event when up arrow key pressed and the first result is in focus', (done) => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
let firstResult: any = element.querySelector('#result_row_0');
|
||||
firstResult.dispatchEvent(new KeyboardEvent('keyup', {
|
||||
key: 'ArrowUp'
|
||||
}));
|
||||
});
|
||||
|
||||
component.scrollBack.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('changing focus', () => {
|
||||
|
||||
let searchService;
|
||||
|
||||
beforeEach(() => {
|
||||
searchService = fixture.debugElement.injector.get(SearchService);
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(result));
|
||||
});
|
||||
|
||||
it('should emit a focus event when a result comes into focus', (done) => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
(<any> element.querySelector('#result_row_0')).dispatchEvent(new FocusEvent('focus'));
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
|
||||
component.searchFocus.subscribe((e: FocusEvent) => {
|
||||
expect(e).not.toBeNull();
|
||||
expect(e.type).toBe('focus');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit a focus event when a result loses focus', (done) => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
(<any> element.querySelector('#result_row_0')).dispatchEvent(new FocusEvent('blur'));
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
|
||||
component.searchFocus.subscribe((e: FocusEvent) => {
|
||||
expect(e).not.toBeNull();
|
||||
expect(e.type).toBe('blur');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should give focus to the first result when focusResult() is called externally', (done) => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
let firstResult: any = element.querySelector('#result_row_0');
|
||||
spyOn(firstResult, 'focus');
|
||||
component.focusResult();
|
||||
expect(firstResult.focus).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
updateSearchTerm('searchTerm');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@@ -1,229 +0,0 @@
|
||||
/*!
|
||||
* @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 { animate, AnimationEvent, state, style, transition, trigger } from '@angular/animations';
|
||||
import { Component, ElementRef, EventEmitter, Input, OnChanges, Output, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { SearchOptions, SearchService } from 'ng2-alfresco-core';
|
||||
import { ThumbnailService } from 'ng2-alfresco-core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-autocomplete',
|
||||
templateUrl: './search-autocomplete.component.html',
|
||||
styleUrls: ['./search-autocomplete.component.scss'],
|
||||
animations: [
|
||||
trigger('transformAutocomplete', [
|
||||
state('void', style({
|
||||
opacity: 0,
|
||||
transform: 'scale(0.01, 0.01)'
|
||||
})),
|
||||
state('enter-start', style({
|
||||
opacity: 1,
|
||||
transform: 'scale(1, 0.5)'
|
||||
})),
|
||||
state('enter', style({
|
||||
transform: 'scale(1, 1)'
|
||||
})),
|
||||
transition('void => enter-start', animate('100ms linear')),
|
||||
transition('enter-start => enter', animate('300ms cubic-bezier(0.25, 0.8, 0.25, 1)')),
|
||||
transition('* => void', animate('150ms 50ms linear', style({opacity: 0})))
|
||||
])
|
||||
],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SearchAutocompleteComponent implements OnChanges {
|
||||
|
||||
@Input()
|
||||
searchTerm: string = '';
|
||||
|
||||
results: any = null;
|
||||
|
||||
errorMessage: string = null;
|
||||
|
||||
@Input()
|
||||
ngClass: any;
|
||||
|
||||
@Input()
|
||||
maxResults: number = 5;
|
||||
|
||||
@Input()
|
||||
resultSort: string = null;
|
||||
|
||||
@Input()
|
||||
rootNodeId: string = '-root';
|
||||
|
||||
@Input()
|
||||
resultType: string = null;
|
||||
|
||||
@Input()
|
||||
highlight: boolean = false;
|
||||
|
||||
@Output()
|
||||
fileSelect: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
searchFocus: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
|
||||
|
||||
@Output()
|
||||
cancel = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
resultsLoad = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
scrollBack = new EventEmitter();
|
||||
|
||||
@ViewChild('resultsTableBody', {}) resultsTableBody: ElementRef;
|
||||
|
||||
panelAnimationState: 'void' | 'enter-start' | 'enter' = 'void';
|
||||
|
||||
constructor(private searchService: SearchService,
|
||||
private thumbnailService: ThumbnailService) {
|
||||
}
|
||||
|
||||
ngOnChanges(changes) {
|
||||
if (changes.searchTerm) {
|
||||
this.results = null;
|
||||
this.errorMessage = null;
|
||||
this.displaySearchResults(changes.searchTerm.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and displays search results
|
||||
* @param searchTerm Search query entered by user
|
||||
*/
|
||||
private displaySearchResults(searchTerm) {
|
||||
let searchOpts: SearchOptions = {
|
||||
include: ['path'],
|
||||
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 = results.list.entries.slice(0, this.maxResults);
|
||||
|
||||
if (results && results.list) {
|
||||
this.startAnimation();
|
||||
}
|
||||
|
||||
this.errorMessage = null;
|
||||
this.resultsLoad.emit(this.results);
|
||||
},
|
||||
error => {
|
||||
this.results = null;
|
||||
this.errorMessage = <any> error;
|
||||
this.resultsLoad.error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets thumbnail URL for the given document node.
|
||||
* @param node Node to get URL for.
|
||||
* @returns {string} URL address.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
focusResult(): void {
|
||||
let firstResult: any = this.resultsTableBody.nativeElement.querySelector('tr');
|
||||
firstResult.focus();
|
||||
}
|
||||
|
||||
private getNextElementSibling(node: Element): Element {
|
||||
return node.nextElementSibling;
|
||||
}
|
||||
|
||||
private getPreviousElementSibling(node: Element): Element {
|
||||
return node.previousElementSibling;
|
||||
}
|
||||
|
||||
onItemClick(node: MinimalNodeEntity): void {
|
||||
if (node && node.entry) {
|
||||
this.fileSelect.emit(node);
|
||||
}
|
||||
}
|
||||
|
||||
onRowFocus($event: FocusEvent): void {
|
||||
this.searchFocus.emit($event);
|
||||
}
|
||||
|
||||
onRowBlur($event: FocusEvent): void {
|
||||
this.searchFocus.emit($event);
|
||||
}
|
||||
|
||||
onRowEnter(node: MinimalNodeEntity): void {
|
||||
if (node && node.entry) {
|
||||
if (node.entry.isFile) {
|
||||
this.fileSelect.emit(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onRowArrowDown($event: KeyboardEvent): void {
|
||||
let nextElement: any = this.getNextElementSibling(<Element> $event.target);
|
||||
if (nextElement) {
|
||||
nextElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
onRowArrowUp($event: KeyboardEvent): void {
|
||||
let previousElement: any = this.getPreviousElementSibling(<Element> $event.target);
|
||||
if (previousElement) {
|
||||
previousElement.focus();
|
||||
} else {
|
||||
this.scrollBack.emit($event);
|
||||
}
|
||||
}
|
||||
|
||||
onRowEscape($event: KeyboardEvent): void {
|
||||
this.cancel.emit($event);
|
||||
}
|
||||
|
||||
startAnimation() {
|
||||
this.panelAnimationState = 'enter-start';
|
||||
}
|
||||
|
||||
resetAnimation() {
|
||||
this.panelAnimationState = 'void';
|
||||
}
|
||||
|
||||
onAnimationDone(event: AnimationEvent) {
|
||||
if (event.toState === 'enter-start') {
|
||||
this.panelAnimationState = 'enter';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,43 +1,64 @@
|
||||
<form #f="ngForm" (ngSubmit)="onSearch()" class="adf-search-form">
|
||||
<div class="adf-search-container"
|
||||
[@transitionMessages]="subscriptAnimationState">
|
||||
<a mat-icon-button
|
||||
*ngIf="expandable"
|
||||
id="adf-search-button"
|
||||
(click)="toggleSearchBar()"
|
||||
class="adf-search-button">
|
||||
<mat-icon aria-label="search button">search</mat-icon>
|
||||
</a>
|
||||
<div class="adf-search-field">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
[type]="inputType"
|
||||
[autocomplete]="getAutoComplete()"
|
||||
data-automation-id="search_input"
|
||||
#searchInput
|
||||
id="searchControl"
|
||||
[formControl]="searchControl"
|
||||
[(ngModel)]="searchTerm"
|
||||
(focus)="onFocus($event)"
|
||||
(blur)="onBlur($event)"
|
||||
(keyup.escape)="onEscape()"
|
||||
(keyup.arrowdown)="onArrowDown()">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<adf-search-autocomplete
|
||||
#autocomplete
|
||||
*ngIf="liveSearchEnabled"
|
||||
[searchTerm]="liveSearchTerm"
|
||||
[rootNodeId]="liveSearchRoot"
|
||||
[resultType]="liveSearchResultType"
|
||||
[resultSort]="liveSearchResultSort"
|
||||
[maxResults]="liveSearchMaxResults"
|
||||
[highlight]="highlight"
|
||||
(fileSelect)="onFileClicked($event)"
|
||||
(searchFocus)="onAutoCompleteFocus($event)"
|
||||
(scrollBack)="onAutoCompleteReturn($event)"
|
||||
(cancel)="onAutoCompleteCancel($event)">
|
||||
</adf-search-autocomplete>
|
||||
<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>
|
||||
|
@@ -1,51 +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-button.mat-icon-button {
|
||||
margin-right: 5px;
|
||||
&-search-fixed-text {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&-search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.adf-search-field {
|
||||
max-width: 260px;
|
||||
padding-top: 6px;
|
||||
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-input-underline .mat-input-ripple {
|
||||
background-color: mat-color($background, card);
|
||||
}
|
||||
|
||||
.mat-form-field-underline {
|
||||
background-color: mat-color($background, card);
|
||||
}
|
||||
|
||||
.mat-input-element {
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
padding-bottom: 2px;
|
||||
&-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-search-field .mat-input-infix {
|
||||
padding: 0;
|
||||
&-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-form{
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-valid-search {
|
||||
&-search-autocomplete-item {
|
||||
&:hover {
|
||||
background-color: mat-color($background, 'hover');
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: mat-color($primary, 900);
|
||||
}
|
||||
}
|
||||
|
@@ -15,28 +15,39 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatInputModule, MatListModule } from '@angular/material';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { AlfrescoAuthenticationService, AlfrescoTranslationService, CoreModule, SearchService } from 'ng2-alfresco-core';
|
||||
import { ThumbnailService } from 'ng2-alfresco-core';
|
||||
import { AlfrescoTranslationService, CoreModule, SearchService } from 'ng2-alfresco-core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { noResult, results } from './../assets/search.component.mock';
|
||||
import { TranslationMock } from './../assets/translation.service.mock';
|
||||
import { SearchAutocompleteComponent } from './search-autocomplete.component';
|
||||
import { SearchControlComponent } from './search-control.component';
|
||||
import { SearchTriggerDirective } from './search-trigger.directive';
|
||||
import { SearchComponent } from './search.component';
|
||||
|
||||
describe('SearchControlComponent', () => {
|
||||
|
||||
let fixture: ComponentFixture<SearchControlComponent>;
|
||||
let component: SearchControlComponent, element: HTMLElement;
|
||||
let component: SearchControlComponent;
|
||||
let element: HTMLElement;
|
||||
let debugElement: DebugElement;
|
||||
let searchService: SearchService;
|
||||
let authService: AlfrescoAuthenticationService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CoreModule
|
||||
CoreModule,
|
||||
MatInputModule,
|
||||
MatListModule
|
||||
],
|
||||
declarations: [
|
||||
SearchControlComponent,
|
||||
SearchAutocompleteComponent
|
||||
SearchComponent,
|
||||
SearchTriggerDirective
|
||||
],
|
||||
providers: [
|
||||
{provide: AlfrescoTranslationService, useClass: TranslationMock},
|
||||
@@ -45,72 +56,93 @@ describe('SearchControlComponent', () => {
|
||||
]
|
||||
}).compileComponents().then(() => {
|
||||
fixture = TestBed.createComponent(SearchControlComponent);
|
||||
debugElement = fixture.debugElement;
|
||||
searchService = TestBed.get(SearchService);
|
||||
authService = TestBed.get(AlfrescoAuthenticationService);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.nativeElement;
|
||||
});
|
||||
}));
|
||||
|
||||
it('should emit searchChange when search term input changed', (done) => {
|
||||
fixture.componentInstance.searchChange.subscribe(e => {
|
||||
expect(e.value).toBe('customSearchTerm');
|
||||
done();
|
||||
});
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.searchTerm = 'customSearchTerm';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
beforeEach(async(() => {
|
||||
spyOn(authService, 'isLoggedIn').and.returnValue(true);
|
||||
}));
|
||||
|
||||
it('should emit searchChange when search term changed by user', (done) => {
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.searchChange.subscribe(e => {
|
||||
expect(e.value).toBe('customSearchTerm211');
|
||||
expect(e.valid).toBe(true);
|
||||
done();
|
||||
});
|
||||
component.searchControl.setValue('customSearchTerm211');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
afterEach(async(() => {
|
||||
fixture.destroy();
|
||||
TestBed.resetTestingModule();
|
||||
}));
|
||||
|
||||
it('should update FAYT search when user inputs a valid term', (done) => {
|
||||
fixture.componentInstance.searchChange.subscribe(() => {
|
||||
expect(fixture.componentInstance.liveSearchTerm).toBe('customSearchTerm');
|
||||
done();
|
||||
});
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.searchTerm = 'customSearchTerm';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
describe('when input values are inserted', () => {
|
||||
|
||||
it('should NOT update FAYT term when user inputs a search term less than 3 characters', (done) => {
|
||||
fixture.componentInstance.searchChange.subscribe(() => {
|
||||
expect(fixture.componentInstance.liveSearchTerm).toBe('');
|
||||
done();
|
||||
});
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.searchTerm = 'cu';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
beforeEach(async(() => {
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should still fire an event when user inputs a search term less than 3 characters', (done) => {
|
||||
fixture.componentInstance.searchChange.subscribe((e) => {
|
||||
expect(e.value).toBe('cu');
|
||||
expect(e.valid).toBe(false);
|
||||
done();
|
||||
});
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.searchTerm = 'cu';
|
||||
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;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
component.expandable = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('search button should be hide', () => {
|
||||
@@ -119,288 +151,279 @@ describe('SearchControlComponent', () => {
|
||||
});
|
||||
|
||||
it('should not have animation', () => {
|
||||
component.ngOnInit();
|
||||
expect(component.subscriptAnimationState).toBe('no-animation');
|
||||
});
|
||||
});
|
||||
|
||||
describe('component rendering', () => {
|
||||
|
||||
it('should display a text input field by default', () => {
|
||||
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 display a search input field when specified', () => {
|
||||
fixture.componentInstance.inputType = 'search';
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelectorAll('input[type="search"]').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should set browser autocomplete to off by default', () => {
|
||||
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 set browser autocomplete to on when configured', () => {
|
||||
fixture.componentInstance.autocomplete = true;
|
||||
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', () => {
|
||||
|
||||
let inputEl: HTMLInputElement;
|
||||
|
||||
beforeEach(() => {
|
||||
inputEl = element.querySelector('input');
|
||||
});
|
||||
|
||||
it('should display a autocomplete list control by default', () => {
|
||||
it('should make autocomplete list control hidden initially', async(() => {
|
||||
fixture.detectChanges();
|
||||
let autocomplete: Element = element.querySelector('adf-search-autocomplete');
|
||||
expect(autocomplete).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should make autocomplete list control hidden initially', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component.liveSearchComponent.panelAnimationState).toBe('void');
|
||||
});
|
||||
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(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(results));
|
||||
|
||||
component.liveSearchTerm = 'test';
|
||||
|
||||
spyOn(component, 'isSearchBarActive').and.returnValue(true);
|
||||
spyOn(searchService, 'getNodeQueryResults').and.returnValue(Observable.of(results));
|
||||
fixture.detectChanges();
|
||||
inputEl.dispatchEvent(new FocusEvent('focus'));
|
||||
window.setTimeout(() => {
|
||||
|
||||
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();
|
||||
expect(component.liveSearchComponent.panelAnimationState).not.toBe('void');
|
||||
let resultElement: Element = element.querySelector('#adf-search-results');
|
||||
let resultElement: Element = element.querySelector('#autocomplete-search-result-list');
|
||||
expect(resultElement).not.toBe(null);
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should show autocomplete list noe results cwhen search box has focus and there is search result with length 0', (done) => {
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(noResult));
|
||||
|
||||
component.liveSearchTerm = 'test';
|
||||
|
||||
fixture.detectChanges();
|
||||
inputEl.dispatchEvent(new FocusEvent('focus'));
|
||||
window.setTimeout(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.liveSearchComponent.panelAnimationState).not.toBe('void');
|
||||
let noResultElement: Element = element.querySelector('#search_no_result');
|
||||
expect(noResultElement).not.toBe(null);
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should hide autocomplete list results when the search box loses focus', (done) => {
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(results));
|
||||
|
||||
component.liveSearchTerm = 'test';
|
||||
|
||||
fixture.detectChanges();
|
||||
inputEl.dispatchEvent(new FocusEvent('focus'));
|
||||
inputEl.dispatchEvent(new FocusEvent('blur'));
|
||||
window.setTimeout(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.liveSearchComponent.panelAnimationState).toBe('void');
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should keep autocomplete list control visible when user tabs into results', (done) => {
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(results));
|
||||
|
||||
component.liveSearchTerm = 'test';
|
||||
|
||||
fixture.detectChanges();
|
||||
inputEl.dispatchEvent(new FocusEvent('focus'));
|
||||
fixture.detectChanges();
|
||||
component.onAutoCompleteFocus(new FocusEvent('focus'));
|
||||
window.setTimeout(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.liveSearchComponent.panelAnimationState).not.toBe('void');
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should hide autocomplete list results when escape key pressed', () => {
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(results));
|
||||
|
||||
component.liveSearchTerm = 'test';
|
||||
|
||||
fixture.detectChanges();
|
||||
inputEl.dispatchEvent(new Event('focus'));
|
||||
inputEl.dispatchEvent(new KeyboardEvent('keyup', {
|
||||
key: 'Escape'
|
||||
}));
|
||||
fixture.detectChanges();
|
||||
expect(component.liveSearchComponent.panelAnimationState).toBe('void');
|
||||
});
|
||||
|
||||
it('should select the first result in autocomplete list when down arrow is pressed and autocomplete list is visible', (done) => {
|
||||
fixture.detectChanges();
|
||||
spyOn(component.liveSearchComponent, 'focusResult');
|
||||
fixture.detectChanges();
|
||||
inputEl.dispatchEvent(new Event('focus'));
|
||||
window.setTimeout(() => {
|
||||
fixture.detectChanges();
|
||||
inputEl.dispatchEvent(new KeyboardEvent('keyup', {
|
||||
key: 'ArrowDown'
|
||||
}));
|
||||
fixture.detectChanges();
|
||||
expect(component.liveSearchComponent.focusResult).toHaveBeenCalled();
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should focus input element when autocomplete list returns control', () => {
|
||||
fixture.detectChanges();
|
||||
spyOn(inputEl, 'focus');
|
||||
fixture.detectChanges();
|
||||
component.onAutoCompleteReturn();
|
||||
expect(inputEl.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should focus input element when autocomplete list is cancelled', () => {
|
||||
fixture.detectChanges();
|
||||
spyOn(inputEl, 'focus');
|
||||
fixture.detectChanges();
|
||||
component.onAutoCompleteCancel();
|
||||
expect(inputEl.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT display a autocomplete list control when configured not to', () => {
|
||||
fixture.componentInstance.liveSearchEnabled = false;
|
||||
fixture.detectChanges();
|
||||
let autocomplete: Element = element.querySelector('adf-search-autocomplete');
|
||||
expect(autocomplete).toBeNull();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('search submit', () => {
|
||||
|
||||
it('should fire a search when a term has been entered', () => {
|
||||
spyOn(component.searchSubmit, 'emit');
|
||||
fixture.detectChanges();
|
||||
let formEl: HTMLElement = element.querySelector('form');
|
||||
component.searchTerm = 'searchTerm1';
|
||||
component.searchControl.setValue('searchTerm1');
|
||||
fixture.detectChanges();
|
||||
formEl.dispatchEvent(new Event('submit'));
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.searchSubmit.emit).toHaveBeenCalledWith({
|
||||
'value': 'searchTerm1'
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fire a search when no term has been entered', () => {
|
||||
spyOn(component.searchSubmit, 'emit');
|
||||
fixture.detectChanges();
|
||||
let inputEl: HTMLInputElement = <HTMLInputElement> element.querySelector('input[type="text"]');
|
||||
let formEl: HTMLElement = element.querySelector('form');
|
||||
inputEl.value = '';
|
||||
formEl.dispatchEvent(new Event('submit'));
|
||||
|
||||
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();
|
||||
|
||||
expect(component.searchSubmit.emit).not.toHaveBeenCalled();
|
||||
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('click on the search button should close the input box when is open', (done) => {
|
||||
fixture.detectChanges();
|
||||
it('should NOT display a autocomplete list control when configured not to', async(() => {
|
||||
component.subscriptAnimationState = 'active';
|
||||
fixture.detectChanges();
|
||||
|
||||
let searchButton: any = element.querySelector('#adf-search-button');
|
||||
searchButton.click();
|
||||
|
||||
setTimeout(() => {
|
||||
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');
|
||||
done();
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('click on the search button should open the input box when is close', (done) => {
|
||||
fixture.detectChanges();
|
||||
component.subscriptAnimationState = 'inactive';
|
||||
|
||||
let searchButton: any = element.querySelector('#adf-search-button');
|
||||
searchButton.click();
|
||||
|
||||
setTimeout(() => {
|
||||
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();
|
||||
}, 300);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
it('Search button should not change the input state too often', (done) => {
|
||||
fixture.detectChanges();
|
||||
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();
|
||||
|
||||
let searchButton: any = element.querySelector('#adf-search-button');
|
||||
searchButton.click();
|
||||
searchButton.click();
|
||||
|
||||
setTimeout(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.subscriptAnimationState).toBe('inactive');
|
||||
done();
|
||||
}, 400);
|
||||
|
||||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('file preview', () => {
|
||||
describe('option click', () => {
|
||||
|
||||
it('should emit a file select event when onFileClicked is called', () => {
|
||||
spyOn(component.fileSelect, 'emit');
|
||||
component.onFileClicked({
|
||||
value: 'node12345'
|
||||
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');
|
||||
});
|
||||
expect(component.fileSelect.emit).toHaveBeenCalledWith({
|
||||
'value': 'node12345'
|
||||
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 file/folder is clicked', (done) => {
|
||||
component.subscriptAnimationState = 'active';
|
||||
component.onFileClicked({
|
||||
value: 'node12345'
|
||||
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();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(component.subscriptAnimationState).toBe('inactive');
|
||||
done();
|
||||
}, 300);
|
||||
let inputDebugElement = fixture.debugElement.query(By.css('#adf-control-input'));
|
||||
inputDebugElement.nativeElement.value = 'TEST';
|
||||
inputDebugElement.nativeElement.focus();
|
||||
inputDebugElement.nativeElement.dispatchEvent(new Event('input'));
|
||||
|
||||
});
|
||||
|
||||
it('should NOT reset the search term after file/folder is clicked', () => {
|
||||
component.liveSearchTerm = 'test';
|
||||
component.onFileClicked({
|
||||
value: 'node12345'
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let firstOption: DebugElement = fixture.debugElement.query(By.css('#result_name_0'));
|
||||
firstOption.triggerEventHandler('click', null);
|
||||
});
|
||||
}));
|
||||
|
||||
expect(component.liveSearchTerm).toBe('test');
|
||||
});
|
||||
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);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@@ -16,24 +16,11 @@
|
||||
*/
|
||||
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { FormControl, Validators } from '@angular/forms';
|
||||
import 'rxjs/add/operator/debounceTime';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { MinimalNodeEntity } from 'alfresco-js-api';
|
||||
import { AlfrescoAuthenticationService, ThumbnailService } from 'ng2-alfresco-core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { SearchTermValidator } from './../forms/search-term-validator';
|
||||
import { SearchAutocompleteComponent } from './search-autocomplete.component';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-control',
|
||||
@@ -41,56 +28,33 @@ import { SearchAutocompleteComponent } from './search-autocomplete.component';
|
||||
styleUrls: ['./search-control.component.scss'],
|
||||
animations: [
|
||||
trigger('transitionMessages', [
|
||||
state('active', style({ transform: 'translateX(0%)' })),
|
||||
state('inactive', style({ transform: 'translateX(83%)' })),
|
||||
state('no-animation', style({ transform: 'translateX(0%)', width: '100%' })),
|
||||
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)'))
|
||||
])
|
||||
],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
]
|
||||
})
|
||||
export class SearchControlComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input()
|
||||
searchTerm = '';
|
||||
|
||||
@Input()
|
||||
inputType = 'text';
|
||||
|
||||
@Input()
|
||||
autocomplete: boolean = false;
|
||||
|
||||
@Input()
|
||||
expandable: boolean = true;
|
||||
|
||||
@Input()
|
||||
highlight: boolean = false;
|
||||
|
||||
@Output()
|
||||
searchChange = new EventEmitter();
|
||||
@Input()
|
||||
inputType: string = 'text';
|
||||
|
||||
@Output()
|
||||
searchSubmit = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
fileSelect = new EventEmitter();
|
||||
|
||||
searchControl: FormControl;
|
||||
|
||||
@ViewChild('searchInput', {})
|
||||
searchInput: ElementRef;
|
||||
|
||||
@ViewChild(SearchAutocompleteComponent)
|
||||
liveSearchComponent: SearchAutocompleteComponent;
|
||||
@Input()
|
||||
autocomplete: boolean = false;
|
||||
|
||||
@Input()
|
||||
liveSearchEnabled: boolean = true;
|
||||
|
||||
liveSearchTerm: string = '';
|
||||
|
||||
@Input()
|
||||
liveSearchRoot: string = '-root-';
|
||||
|
||||
@@ -103,21 +67,25 @@ export class SearchControlComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
liveSearchMaxResults: number = 5;
|
||||
|
||||
searchValid = false;
|
||||
@Output()
|
||||
submit: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
private focusSubject = new Subject<FocusEvent>();
|
||||
@Output()
|
||||
searchChange: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
private toggleSearch = new Subject<any>();
|
||||
@Output()
|
||||
optionClicked: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
searchTerm: string = '';
|
||||
subscriptAnimationState: string;
|
||||
|
||||
constructor() {
|
||||
this.searchControl = new FormControl(
|
||||
this.searchTerm,
|
||||
Validators.compose([Validators.required, SearchTermValidator.minAlphanumericChars(3)])
|
||||
);
|
||||
private toggleSearch = new Subject<any>();
|
||||
private focusSubject = new Subject<FocusEvent>();
|
||||
|
||||
this.toggleSearch.asObservable().debounceTime(200).subscribe(() => {
|
||||
constructor(public authService: AlfrescoAuthenticationService,
|
||||
private thumbnailService: ThumbnailService) {
|
||||
|
||||
this.toggleSearch.asObservable().debounceTime(100).subscribe(() => {
|
||||
if (this.expandable) {
|
||||
this.subscriptAnimationState = this.subscriptAnimationState === 'inactive' ? 'active' : 'inactive';
|
||||
|
||||
@@ -126,115 +94,85 @@ export class SearchControlComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.searchControl.valueChanges.subscribe((value: string) => {
|
||||
if (value) {
|
||||
this.onSearchTermChange(value);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
ngOnInit() {
|
||||
this.subscriptAnimationState = this.expandable ? 'inactive' : 'no-animation';
|
||||
this.setupFocusEventHandlers();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.focusSubject.unsubscribe();
|
||||
this.toggleSearch.unsubscribe();
|
||||
}
|
||||
|
||||
private onSearchTermChange(value: string): void {
|
||||
this.searchValid = this.searchControl.valid;
|
||||
this.liveSearchTerm = this.searchValid ? value : '';
|
||||
this.searchChange.emit({
|
||||
value: value,
|
||||
valid: this.searchValid
|
||||
});
|
||||
isLoggedIn(): boolean {
|
||||
return this.authService.isLoggedIn();
|
||||
}
|
||||
|
||||
private setupFocusEventHandlers() {
|
||||
let focusEvents: Observable<FocusEvent> = this.focusSubject.asObservable().debounceTime(50);
|
||||
searchSubmit(event: any) {
|
||||
this.submit.emit(event);
|
||||
this.toggleSearchBar();
|
||||
}
|
||||
|
||||
focusEvents.filter(($event: any) => {
|
||||
return $event.type === 'focusout' || $event.type === 'blur';
|
||||
}).subscribe(() => {
|
||||
this.onSearchBlur();
|
||||
});
|
||||
inputChange(event: any) {
|
||||
this.searchChange.emit(event);
|
||||
}
|
||||
|
||||
getAutoComplete(): string {
|
||||
return this.autocomplete ? 'on' : 'off';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called on form submit, i.e. when the user has hit enter
|
||||
*
|
||||
* @param event Submit event that was fired
|
||||
*/
|
||||
onSearch(): void {
|
||||
this.searchControl.setValue(this.searchTerm);
|
||||
if (this.searchControl.valid) {
|
||||
this.searchSubmit.emit({
|
||||
value: this.searchTerm
|
||||
});
|
||||
this.searchInput.nativeElement.blur();
|
||||
}
|
||||
}
|
||||
getMimeTypeIcon(node: MinimalNodeEntity): string {
|
||||
let mimeType;
|
||||
|
||||
hideAutocomplete(): void {
|
||||
if (this.liveSearchComponent) {
|
||||
this.liveSearchComponent.resetAnimation();
|
||||
}
|
||||
}
|
||||
if (node.entry.content && node.entry.content.mimeType) {
|
||||
mimeType = node.entry.content.mimeType;
|
||||
}
|
||||
if (node.entry.isFolder) {
|
||||
mimeType = 'folder';
|
||||
}
|
||||
|
||||
onFileClicked(event): void {
|
||||
this.hideAutocomplete();
|
||||
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();
|
||||
this.fileSelect.emit(event);
|
||||
this.searchTerm = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSearchBlur(): void {
|
||||
this.hideAutocomplete();
|
||||
this.toggleSearchBar();
|
||||
}
|
||||
|
||||
onFocus($event): void {
|
||||
this.focusSubject.next($event);
|
||||
}
|
||||
|
||||
onBlur($event): void {
|
||||
this.focusSubject.next($event);
|
||||
}
|
||||
|
||||
onEscape(): void {
|
||||
this.hideAutocomplete();
|
||||
this.toggleSearchBar();
|
||||
}
|
||||
|
||||
onArrowDown(): void {
|
||||
this.liveSearchComponent.focusResult();
|
||||
}
|
||||
|
||||
onAutoCompleteFocus($event): void {
|
||||
this.focusSubject.next($event);
|
||||
}
|
||||
|
||||
onAutoCompleteReturn(): void {
|
||||
if (this.searchInput) {
|
||||
(<any> this.searchInput.nativeElement).focus();
|
||||
}
|
||||
}
|
||||
|
||||
onAutoCompleteCancel(): void {
|
||||
if (this.searchInput) {
|
||||
(<any> this.searchInput.nativeElement).focus();
|
||||
}
|
||||
this.hideAutocomplete();
|
||||
}
|
||||
|
||||
toggleSearchBar() {
|
||||
this.toggleSearch.next();
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -1,81 +1,8 @@
|
||||
<div data-automation-id="search_result_table"
|
||||
class="adf-data-table full-width">
|
||||
<p data-automation-id="search_error_message" *ngIf="errorMessage">{{ 'SEARCH.RESULTS.ERROR' | translate:{errorMessage: errorMessage} }}</p>
|
||||
<div class="container">
|
||||
<adf-document-list
|
||||
[node]="nodeResults"
|
||||
[contextMenuActions]="true"
|
||||
[contentActions]="true"
|
||||
[navigationMode]="navigationMode"
|
||||
[navigate]="navigate"
|
||||
[enablePagination]="false"
|
||||
(nodeDblClick)="onDoubleClick($event)"
|
||||
(preview)="onPreviewFile($event)">
|
||||
<empty-folder-content>
|
||||
<ng-template>
|
||||
<div class="empty_template">
|
||||
<div class="no-result-message">{{ 'SEARCH.RESULTS.NONE' | translate:{searchTerm: searchTerm} }}</div>
|
||||
<img [src]="emptyFolderImageUrl" class="no-result__empty_doc_lib">
|
||||
</div>
|
||||
</ng-template>
|
||||
</empty-folder-content>
|
||||
<data-columns>
|
||||
<data-column key="$thumbnail" type="image"></data-column>
|
||||
<data-column
|
||||
title="{{'SEARCH.DOCUMENT_LIST.COLUMNS.DISPLAY_NAME' | translate}}"
|
||||
key="name"
|
||||
sortable="true"
|
||||
class="full-width ellipsis-cell">
|
||||
</data-column>
|
||||
<data-column
|
||||
title="{{'SEARCH.DOCUMENT_LIST.COLUMNS.CREATED_BY' | translate}}"
|
||||
key="createdByUser.displayName"
|
||||
sortable="true"
|
||||
class="desktop-only">
|
||||
</data-column>
|
||||
<data-column
|
||||
title="{{'SEARCH.DOCUMENT_LIST.COLUMNS.CREATED_ON' | translate}}"
|
||||
key="createdAt"
|
||||
type="date"
|
||||
format="medium"
|
||||
sortable="true"
|
||||
class="desktop-only">
|
||||
</data-column>
|
||||
</data-columns>
|
||||
|
||||
<content-actions>
|
||||
<!-- folder actions -->
|
||||
<content-action
|
||||
target="folder"
|
||||
title="{{'SEARCH.DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}"
|
||||
permission="delete"
|
||||
handler="delete"
|
||||
(permissionEvent)="handlePermission($event)">
|
||||
</content-action>
|
||||
<!-- document actions -->
|
||||
<content-action
|
||||
target="document"
|
||||
title="{{'SEARCH.DOCUMENT_LIST.ACTIONS.DOCUMENT.DOWNLOAD' | translate}}"
|
||||
handler="download">
|
||||
</content-action>
|
||||
<content-action
|
||||
target="document"
|
||||
title="{{'SEARCH.DOCUMENT_LIST.ACTIONS.DOCUMENT.DELETE' | translate}}"
|
||||
permission="delete"
|
||||
handler="delete"
|
||||
(execute)="onContentDelete($event)"
|
||||
(permissionEvent)="handlePermission($event)">
|
||||
</content-action>
|
||||
</content-actions>
|
||||
</adf-document-list>
|
||||
|
||||
<adf-pagination
|
||||
(changePageSize)="onChangePageSize($event)"
|
||||
(nextPage)="onNextPage($event)"
|
||||
(prevPage)="onPrevPage($event)"
|
||||
[pagination]="pagination"
|
||||
[maxItems]="maxResults"
|
||||
[supportedPageSizes]="[5, 10, 15, 20]">
|
||||
</adf-pagination>
|
||||
</div>
|
||||
<div role="listbox" id="adf-search-results-content" [ngClass]="_classList" #panel>
|
||||
<ng-template
|
||||
[ngTemplateOutlet]="template"
|
||||
[ngTemplateOutletContext]="{ $implicit: results }">
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
|
||||
|
@@ -1,37 +1,19 @@
|
||||
:host .adf-data-table caption {
|
||||
margin: 0 0 16px 0;
|
||||
text-align: left;
|
||||
}
|
||||
:host .adf-data-table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
:host .adf-data-table td.col-mimetype-icon {
|
||||
width: 24px;
|
||||
}
|
||||
:host .col-display-name {
|
||||
min-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@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;
|
||||
|
||||
.no-result-message {
|
||||
height: 32px;
|
||||
opacity: 0.26;
|
||||
font-size: 24px;
|
||||
line-height: 1.33;
|
||||
letter-spacing: -1px;
|
||||
color: #000000;
|
||||
}
|
||||
.adf {
|
||||
|
||||
.no-result__empty_doc_lib {
|
||||
width: 565px;
|
||||
height: 161px;
|
||||
object-fit: contain;
|
||||
margin-top: 17px;
|
||||
}
|
||||
&-search-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.empty_template {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
&-search-show {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,377 +15,101 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DebugElement, ReflectiveInjector, SimpleChange } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AlfrescoTranslationService, CoreModule, NotificationService, SearchService } from 'ng2-alfresco-core';
|
||||
import { DocumentListModule } from 'ng2-alfresco-documentlist';
|
||||
import { PermissionModel } from 'ng2-alfresco-documentlist';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { TranslationMock } from './../assets/translation.service.mock';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { CoreModule, SearchService } from 'ng2-alfresco-core';
|
||||
import { SearchModule } from '../../index';
|
||||
import { differentResult, result, SimpleSearchTestComponent } from './../assets/search.component.mock';
|
||||
|
||||
describe('SearchComponent', () => {
|
||||
|
||||
let fixture: ComponentFixture<SearchComponent>, element: HTMLElement;
|
||||
let component: SearchComponent;
|
||||
|
||||
let result = {
|
||||
list: {
|
||||
pagination: {
|
||||
hasMoreItems: false,
|
||||
maxItems: 25,
|
||||
skipCount: 0,
|
||||
totalItems: 1
|
||||
},
|
||||
entries: [
|
||||
{
|
||||
entry: {
|
||||
id: '123',
|
||||
name: 'MyDoc',
|
||||
isFile: true,
|
||||
content: {
|
||||
mimeType: 'text/plain'
|
||||
},
|
||||
createdByUser: {
|
||||
displayName: 'John Doe'
|
||||
},
|
||||
modifiedByUser: {
|
||||
displayName: 'John Doe'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
let folderResult = {
|
||||
list: {
|
||||
pagination: {
|
||||
hasMoreItems: false,
|
||||
maxItems: 25,
|
||||
skipCount: 0,
|
||||
totalItems: 1
|
||||
},
|
||||
entries: [
|
||||
{
|
||||
entry: {
|
||||
id: '123',
|
||||
name: 'MyFolder',
|
||||
isFile: false,
|
||||
isFolder: true,
|
||||
createdByUser: {
|
||||
displayName: 'John Doe'
|
||||
},
|
||||
modifiedByUser: {
|
||||
displayName: 'John Doe'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
let noResult = {
|
||||
list: {
|
||||
pagination: {
|
||||
hasMoreItems: false,
|
||||
maxItems: 25,
|
||||
skipCount: 0,
|
||||
totalItems: 0
|
||||
},
|
||||
entries: []
|
||||
}
|
||||
};
|
||||
|
||||
let errorJson = {
|
||||
error: {
|
||||
errorKey: 'Search failed',
|
||||
statusCode: 400,
|
||||
briefSummary: '08220082 search failed',
|
||||
stackTrace: 'For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.',
|
||||
descriptionURL: 'https://api-explorer.alfresco.com'
|
||||
}
|
||||
};
|
||||
let fixture: ComponentFixture<SimpleSearchTestComponent>, element: HTMLElement;
|
||||
let component: SimpleSearchTestComponent;
|
||||
let searchService: SearchService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CoreModule,
|
||||
DocumentListModule
|
||||
SearchModule
|
||||
],
|
||||
declarations: [SearchComponent],
|
||||
providers: [
|
||||
SearchService,
|
||||
{provide: AlfrescoTranslationService, useClass: TranslationMock},
|
||||
{provide: NotificationService, useClass: NotificationService}
|
||||
]
|
||||
declarations: [ SimpleSearchTestComponent ]
|
||||
}).compileComponents().then(() => {
|
||||
fixture = TestBed.createComponent(SearchComponent);
|
||||
fixture = TestBed.createComponent(SimpleSearchTestComponent);
|
||||
component = fixture.componentInstance;
|
||||
element = fixture.nativeElement;
|
||||
searchService = TestBed.get(SearchService);
|
||||
});
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
describe('search results', () => {
|
||||
|
||||
it('should not have a search term by default', () => {
|
||||
expect(component.searchTerm).toBe('');
|
||||
});
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should take the provided search term from query param provided via RouteParams', () => {
|
||||
let injector = ReflectiveInjector.resolveAndCreate([
|
||||
{provide: ActivatedRoute, useValue: {params: Observable.from([{q: 'exampleTerm692'}])}}
|
||||
]);
|
||||
|
||||
let search = new SearchComponent(null, null, null, injector.get(ActivatedRoute));
|
||||
|
||||
search.ngOnInit();
|
||||
|
||||
expect(search.searchTerm).toBe('exampleTerm692');
|
||||
});
|
||||
|
||||
it('should show the Notification snackbar on permission error', () => {
|
||||
const notoficationService = TestBed.get(NotificationService);
|
||||
spyOn(notoficationService, 'openSnackMessage');
|
||||
|
||||
component.handlePermission(new PermissionModel());
|
||||
|
||||
expect(notoficationService.openSnackMessage).toHaveBeenCalledWith('PERMISSON.LACKOF', 3000);
|
||||
});
|
||||
|
||||
describe('Search results', () => {
|
||||
|
||||
it('should add wildcard in the search parameters', (done) => {
|
||||
let searchTerm = 'searchTerm6368';
|
||||
let searchTermOut = 'searchTerm6368*';
|
||||
let options = {
|
||||
include: ['path', 'allowableOperations'],
|
||||
skipCount: 0,
|
||||
rootNodeId: '-my-',
|
||||
nodeType: 'my:type',
|
||||
maxItems: 20,
|
||||
orderBy: null
|
||||
};
|
||||
|
||||
component.searchTerm = searchTerm;
|
||||
component.rootNodeId = '-my-';
|
||||
component.resultType = 'my:type';
|
||||
let searchService = fixture.debugElement.injector.get(SearchService);
|
||||
it('should clear results straight away when a new search term is entered', async(() => {
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(result));
|
||||
fixture.detectChanges();
|
||||
.and.returnValues(Promise.resolve(result), Promise.resolve(differentResult));
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
component.setSearchWordTo('searchTerm');
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(searchService.getQueryNodesPromise).toHaveBeenCalledWith(searchTermOut, options);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display search results when a search term is provided', (done) => {
|
||||
let searchService = TestBed.get(SearchService);
|
||||
spyOn(searchService, 'getQueryNodesPromise').and.returnValue(Promise.resolve(result));
|
||||
component.searchTerm = '';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
component.resultsLoad.subscribe(() => {
|
||||
let optionShowed = element.querySelectorAll('#autocomplete-search-result-list > li').length;
|
||||
expect(optionShowed).toBe(1);
|
||||
component.setSearchWordTo('searchTerm2');
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let resultsEl = element.querySelector('[data-automation-id="text_MyDoc"]');
|
||||
expect(resultsEl).not.toBeNull();
|
||||
expect(resultsEl.innerHTML.trim()).toContain('MyDoc');
|
||||
done();
|
||||
optionShowed = element.querySelectorAll('#autocomplete-search-result-list > li').length;
|
||||
expect(optionShowed).toBe(1);
|
||||
});
|
||||
|
||||
component.ngOnChanges({searchTerm: new SimpleChange('', 'searchTerm', true)});
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display no result if no result are returned', (done) => {
|
||||
|
||||
let searchService = TestBed.get(SearchService);
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(noResult));
|
||||
|
||||
component.searchTerm = '';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('.no-result-message')).not.toBeNull();
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({searchTerm: new SimpleChange('', 'searchTerm', true)});
|
||||
});
|
||||
});
|
||||
|
||||
it('should display an error if an error is encountered running the search', (done) => {
|
||||
|
||||
let searchService = TestBed.get(SearchService);
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.reject(errorJson));
|
||||
|
||||
component.searchTerm = '';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
}, () => {
|
||||
fixture.detectChanges();
|
||||
let errorEl = element.querySelector('[data-automation-id="search_error_message"]');
|
||||
expect(errorEl).not.toBeNull();
|
||||
expect((<any> errorEl).innerText).toBe('SEARCH.RESULTS.ERROR');
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({searchTerm: new SimpleChange('', 'searchTerm', true)});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update search results when the search term input is changed', (done) => {
|
||||
|
||||
let searchService = TestBed.get(SearchService);
|
||||
it('should display the returned search results', async(() => {
|
||||
spyOn(searchService, 'getQueryNodesPromise')
|
||||
.and.returnValue(Promise.resolve(result));
|
||||
|
||||
component.searchTerm = '';
|
||||
|
||||
component.setSearchWordTo('searchTerm');
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
component.resultsLoad.subscribe(() => {
|
||||
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();
|
||||
expect(searchService.getQueryNodesPromise).toHaveBeenCalled();
|
||||
let resultsEl = element.querySelector('[data-automation-id="text_MyDoc"]');
|
||||
expect(resultsEl).not.toBeNull();
|
||||
expect(resultsEl.innerHTML.trim()).toContain('MyDoc');
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({searchTerm: new SimpleChange('', 'searchTerm2', true)});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('search result interactions', () => {
|
||||
|
||||
let debugElement: DebugElement;
|
||||
let searchService: SearchService;
|
||||
let querySpy: jasmine.Spy;
|
||||
let emitSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
debugElement = fixture.debugElement;
|
||||
searchService = TestBed.get(SearchService);
|
||||
querySpy = spyOn(searchService, 'getQueryNodesPromise').and.returnValue(Promise.resolve(result));
|
||||
emitSpy = spyOn(component.preview, 'emit');
|
||||
});
|
||||
|
||||
describe('click results', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
component.navigationMode = SearchComponent.SINGLE_CLICK_NAVIGATION;
|
||||
});
|
||||
|
||||
it('should emit preview event when file item clicked', (done) => {
|
||||
|
||||
component.searchTerm = '';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let resultsEl = element.querySelector('[data-automation-id="text_MyDoc"]');
|
||||
resultsEl.dispatchEvent(new Event('click'));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({searchTerm: new SimpleChange('', 'searchTerm', true)});
|
||||
let elementList = element.querySelector('#adf-search-results-content');
|
||||
expect(elementList.classList).toContain('adf-search-hide');
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit preview event when non-file item is clicked', (done) => {
|
||||
querySpy.and.returnValue(Promise.resolve(folderResult));
|
||||
|
||||
component.searchTerm = '';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let resultsEl = element.querySelector('[data-automation-id="text_MyFolder"]');
|
||||
resultsEl.dispatchEvent(new Event('click'));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({searchTerm: new SimpleChange('', 'searchTerm', true)});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('double click results', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
component.navigationMode = SearchComponent.DOUBLE_CLICK_NAVIGATION;
|
||||
});
|
||||
|
||||
it('should emit preview event when file item clicked', (done) => {
|
||||
component.searchTerm = '';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let resultsEl = element.querySelector('[data-automation-id="text_MyDoc"]');
|
||||
resultsEl.dispatchEvent(new Event('dblclick'));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({searchTerm: new SimpleChange('', 'searchTerm', true)});
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit preview event when non-file item is clicked', (done) => {
|
||||
|
||||
querySpy.and.returnValue(Promise.resolve(folderResult));
|
||||
|
||||
component.searchTerm = '';
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
|
||||
component.resultsLoad.subscribe(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let resultsEl = element.querySelector('[data-automation-id="text_MyFolder"]');
|
||||
resultsEl.dispatchEvent(new Event('dblclick'));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({searchTerm: new SimpleChange('', 'searchTerm', true)});
|
||||
});
|
||||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@@ -15,27 +15,47 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Input, OnChanges, OnInit, Optional, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { NodePaging, Pagination } from 'alfresco-js-api';
|
||||
import { AlfrescoTranslationService, NotificationService, SearchOptions, SearchService } from 'ng2-alfresco-core';
|
||||
import { PermissionModel } from 'ng2-alfresco-documentlist';
|
||||
|
||||
declare var require: any;
|
||||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { NodePaging } from 'alfresco-js-api';
|
||||
import { SearchOptions, SearchService } from 'ng2-alfresco-core';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
styleUrls: ['./search.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
preserveWhitespaces: false,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
exportAs: 'searchAutocomplete',
|
||||
host: {
|
||||
'class': 'adf-search'
|
||||
}
|
||||
})
|
||||
export class SearchComponent implements OnChanges, OnInit {
|
||||
export class SearchComponent implements AfterContentInit, OnChanges {
|
||||
|
||||
static SINGLE_CLICK_NAVIGATION: string = 'click';
|
||||
static DOUBLE_CLICK_NAVIGATION: string = 'dblclick';
|
||||
@ViewChild('panel')
|
||||
panel: ElementRef;
|
||||
|
||||
@ContentChild(TemplateRef)
|
||||
template: TemplateRef<any>;
|
||||
|
||||
@Input()
|
||||
searchTerm: string = '';
|
||||
displayWith: ((value: any) => string) | null = null;
|
||||
|
||||
@Input()
|
||||
maxResults: number = 20;
|
||||
@@ -50,121 +70,117 @@ export class SearchComponent implements OnChanges, OnInit {
|
||||
resultType: string = null;
|
||||
|
||||
@Input()
|
||||
navigationMode: string = SearchComponent.DOUBLE_CLICK_NAVIGATION; // click|dblclick
|
||||
searchTerm: string = '';
|
||||
|
||||
@Input()
|
||||
navigate: boolean = true;
|
||||
|
||||
@Input()
|
||||
emptyFolderImageUrl: string = require('../assets/images/empty_doc_lib.svg');
|
||||
|
||||
@Output()
|
||||
resultsLoad = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
preview: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
@Output()
|
||||
nodeDbClick: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
pagination: Pagination;
|
||||
errorMessage;
|
||||
queryParamName = 'q';
|
||||
skipCount: number = 0;
|
||||
nodeResults: NodePaging;
|
||||
|
||||
constructor(private searchService: SearchService,
|
||||
private translateService: AlfrescoTranslationService,
|
||||
private notificationService: NotificationService,
|
||||
@Optional() private route: ActivatedRoute) {
|
||||
@Input('class')
|
||||
set classList(classList: string) {
|
||||
if (classList && classList.length) {
|
||||
classList.split(' ').forEach(className => this._classList[className.trim()] = true);
|
||||
this._elementRef.nativeElement.className = '';
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.route) {
|
||||
this.route.params.forEach((params: Params) => {
|
||||
this.searchTerm = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null;
|
||||
this.displaySearchResults(this.searchTerm);
|
||||
@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);
|
||||
});
|
||||
} else {
|
||||
this.displaySearchResults(this.searchTerm);
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.setVisibility();
|
||||
}
|
||||
|
||||
ngOnChanges(changes) {
|
||||
if (changes.searchTerm) {
|
||||
this.resetResults();
|
||||
this.displaySearchResults(changes.searchTerm.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes['searchTerm']) {
|
||||
this.searchTerm = changes['searchTerm'].currentValue;
|
||||
this.skipCount = 0;
|
||||
this.displaySearchResults(this.searchTerm);
|
||||
resetResults() {
|
||||
this.cleanResults();
|
||||
this.setVisibility();
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.displaySearchResults(this.searchTerm);
|
||||
}
|
||||
|
||||
private cleanResults() {
|
||||
if (this.results) {
|
||||
this.results = {};
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClick($event: any) {
|
||||
if (!this.navigate && $event.value) {
|
||||
this.nodeDbClick.emit({ value: $event.value });
|
||||
}
|
||||
}
|
||||
|
||||
onPreviewFile(event: any) {
|
||||
if (event.value) {
|
||||
this.preview.emit({ value: event.value });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and displays search results
|
||||
* @param searchTerm Search query entered by user
|
||||
*/
|
||||
private displaySearchResults(searchTerm) {
|
||||
if (searchTerm && this.searchService) {
|
||||
let searchOpts: SearchOptions = {
|
||||
include: ['path', 'allowableOperations'],
|
||||
rootNodeId: this.rootNodeId,
|
||||
nodeType: this.resultType,
|
||||
maxItems: this.maxResults,
|
||||
orderBy: this.resultSort
|
||||
};
|
||||
if (searchTerm !== null && searchTerm !== '') {
|
||||
searchTerm = searchTerm + '*';
|
||||
let searchOpts: SearchOptions = {
|
||||
include: ['path', 'allowableOperations'],
|
||||
skipCount: this.skipCount,
|
||||
rootNodeId: this.rootNodeId,
|
||||
nodeType: this.resultType,
|
||||
maxItems: this.maxResults,
|
||||
orderBy: this.resultSort
|
||||
};
|
||||
this.searchService
|
||||
.getNodeQueryResults(searchTerm, searchOpts)
|
||||
.subscribe(
|
||||
results => {
|
||||
this.nodeResults = results;
|
||||
this.pagination = results.list.pagination;
|
||||
this.resultsLoad.emit(results.list.entries);
|
||||
this.errorMessage = null;
|
||||
},
|
||||
error => {
|
||||
if (error.status !== 400) {
|
||||
this.errorMessage = <any> error;
|
||||
this.resultsLoad.error(error);
|
||||
}
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onChangePageSize(event: Pagination): void {
|
||||
this.maxResults = event.maxItems;
|
||||
this.displaySearchResults(this.searchTerm);
|
||||
hidePanel() {
|
||||
if (this.isOpen) {
|
||||
this._classList['adf-search-show'] = false;
|
||||
this._classList['adf-search-hide'] = true;
|
||||
this.isOpen = false;
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
public onNextPage(event: Pagination): void {
|
||||
this.skipCount = event.skipCount;
|
||||
this.displaySearchResults(this.searchTerm);
|
||||
}
|
||||
|
||||
public onPrevPage(event: Pagination): void {
|
||||
this.skipCount = event.skipCount;
|
||||
this.displaySearchResults(this.searchTerm);
|
||||
}
|
||||
|
||||
public onContentDelete(entry: any) {
|
||||
this.displaySearchResults(this.searchTerm);
|
||||
}
|
||||
|
||||
public handlePermission(permission: PermissionModel): void {
|
||||
let permissionErrorMessage: any = this.translateService.get('PERMISSON.LACKOF', permission);
|
||||
this.notificationService.openSnackMessage(permissionErrorMessage.value, 3000);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
@import '../src/components/search-autocomplete.component';
|
||||
@import '../src/components/search.component';
|
||||
@import '../src/components/search-control.component';
|
||||
|
||||
@mixin alfresco-search-theme($theme) {
|
||||
|
Reference in New Issue
Block a user