diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html
index 074c5700d7..4ab9e172bd 100644
--- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html
+++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html
@@ -1,5 +1,5 @@
diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.scss b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.scss
index 595b2aefb8..2212dadb6a 100644
--- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.scss
+++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.scss
@@ -2,11 +2,9 @@
@import '../../../../../styles/mixins.scss';
.filters {
- margin-top: $spacer/2;
- margin-bottom: $spacer/2;
a {
color: $body-color;
- &:hover {
+ &:hover, &focus {
text-decoration: none;
}
}
@@ -15,4 +13,5 @@
text-decoration: underline;
cursor: pointer;
}
-}
\ No newline at end of file
+}
+
diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts
index 5f8111c87b..e722702f88 100644
--- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts
@@ -1,4 +1,12 @@
-import { Component, Input, OnDestroy, OnInit } from '@angular/core';
+import {
+ Component,
+ ElementRef,
+ Input,
+ OnDestroy,
+ OnInit, QueryList,
+ ViewChild,
+ ViewChildren
+} from '@angular/core';
import { FacetValue } from '../../../search-service/facet-value.model';
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
import { Router } from '@angular/router';
@@ -11,6 +19,7 @@ import { SearchService } from '../../../search-service/search.service';
import { SearchOptions } from '../../../search-options.model';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Subscription } from 'rxjs/Subscription';
+import { EmphasizePipe } from '../../../../shared/utils/emphasize.pipe';
/**
* This component renders a simple item page.
@@ -34,6 +43,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
filter: string;
pageChange = false;
sub: Subscription;
+ filterSearchResults: Observable = Observable.of([]);
constructor(private searchService: SearchService, private filterService: SearchFilterService, private router: Router) {
}
@@ -94,11 +104,17 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
});
this.filter = '';
}
+ this.filterSearchResults = Observable.of([]);
+ }
+
+ clickFilter(data: string) {
+ this.onSubmit({ [this.filterConfig.paramName]: data });
}
hasValue(o: any): boolean {
return hasValue(o);
}
+
getRemoveParams(value: string) {
return {
[this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value),
@@ -122,4 +138,26 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
this.sub.unsubscribe();
}
}
+
+ findSuggestions(data): void {
+ if (isNotEmpty(data)) {
+ this.filterService.getSearchOptions().first().subscribe(
+ (options) => {
+ this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase())
+ .first()
+ .map(
+ (rd: RemoteData>) => {
+ return rd.payload.page.map((facet) => facet.value)
+ }
+ );
+ }
+ )
+ } else {
+ this.filterSearchResults = Observable.of([]);
+ }
+ }
+
+ getDisplayValue(value: string, searchValue: string) {
+ return new EmphasizePipe().transform(value, searchValue);
+ }
}
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.scss b/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
index f694e9e167..65b2ccfff6 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
@@ -3,6 +3,9 @@
:host {
border: 1px solid map-get($theme-colors, light);
+ >div {
+ position: relative;
+ }
.search-filter-wrapper {
overflow: hidden;
}
diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts
index b51a9c834d..774d2287e2 100644
--- a/src/app/+search-page/search-service/search.service.ts
+++ b/src/app/+search-page/search-service/search.service.ts
@@ -175,13 +175,18 @@ export class SearchService implements OnDestroy {
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs);
}
- getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions): Observable>> {
+ getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions, filterQuery?: string): Observable>> {
const requestObs = this.halService.getEndpoint(this.facetValueLinkPathPrefix + filterConfig.name).pipe(
map((url: string) => {
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
+ if (hasValue(filterQuery)) {
+ // args.push(`${filterConfig.paramName}=${filterQuery},query`);
+ args.push(`prefix=${filterQuery}`);
+ }
if (hasValue(searchOptions)) {
url = searchOptions.toRestUrl(url, args);
}
+
const request = new GetRequest(this.requestService.generateRequestId(), url);
return Object.assign(request, {
getResponseParser(): GenericConstructor {
diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts
index c7456aa2f9..ebefbe4ba4 100644
--- a/src/app/core/data/search-response-parsing.service.ts
+++ b/src/app/core/data/search-response-parsing.service.ts
@@ -16,7 +16,7 @@ export class SearchResponseParsingService implements ResponseParsingService {
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
- const payload = data.payload;
+ const payload = data.payload._embedded.searchResult;
const hitHighlights = payload._embedded.objects
.map((object) => object.hitHighlights)
.map((hhObject) => {
diff --git a/src/app/shared/input-suggestions/input-suggestions.component.html b/src/app/shared/input-suggestions/input-suggestions.component.html
new file mode 100644
index 0000000000..26bb975b5e
--- /dev/null
+++ b/src/app/shared/input-suggestions/input-suggestions.component.html
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/src/app/shared/input-suggestions/input-suggestions.component.scss b/src/app/shared/input-suggestions/input-suggestions.component.scss
new file mode 100644
index 0000000000..ceda9c75b5
--- /dev/null
+++ b/src/app/shared/input-suggestions/input-suggestions.component.scss
@@ -0,0 +1,7 @@
+.autocomplete {
+ width: 100%;
+ .dropdown-item {
+ white-space: normal;
+ word-break: break-word;
+ }
+}
\ No newline at end of file
diff --git a/src/app/shared/input-suggestions/input-suggestions.component.ts b/src/app/shared/input-suggestions/input-suggestions.component.ts
new file mode 100644
index 0000000000..166cfd71d2
--- /dev/null
+++ b/src/app/shared/input-suggestions/input-suggestions.component.ts
@@ -0,0 +1,95 @@
+import {
+ Component,
+ ElementRef, EventEmitter,
+ Input,
+ Output,
+ QueryList, SimpleChanges,
+ ViewChild,
+ ViewChildren
+} from '@angular/core';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import { hasValue, isNotEmpty } from '../empty.util';
+
+/**
+ * This component renders a simple item page.
+ * The route parameter 'id' is used to request the item it represents.
+ * All fields of the item that should be displayed, are defined in its template.
+ */
+
+@Component({
+ selector: 'ds-input-suggestions',
+ styleUrls: ['./input-suggestions.component.scss'],
+ templateUrl: './input-suggestions.component.html'
+})
+
+export class InputSuggestionsComponent {
+ @Input() suggestions: string[] = [];
+ @Input() debounceTime = 500;
+ @Input() placeholder = '';
+ @Input() action;
+ @Input() name;
+ @Input() ngModel;
+ @Output() ngModelChange = new EventEmitter();
+ @Output() submitSuggestion = new EventEmitter();
+ @Output() clickSuggestion = new EventEmitter();
+ @Output() findSuggestions = new EventEmitter();
+ show = new BehaviorSubject(false);
+ selectedIndex = -1;
+ @ViewChild('inputField') queryInput: ElementRef;
+ @ViewChildren('suggestion') resultViews: QueryList;
+ @Input() getDisplayValue: (value: string, query: string) => string = (value: string, query: string) => value;
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (hasValue(changes.suggestions)) {
+ this.show.next(isNotEmpty(changes.suggestions.currentValue));
+ }
+ }
+
+ shiftFocusUp(event: KeyboardEvent) {
+ event.preventDefault();
+ if (this.selectedIndex > 0) {
+ this.selectedIndex--;
+ this.selectedIndex = (this.selectedIndex + this.resultViews.length) % this.resultViews.length; // Prevent negative modulo outcome
+ } else {
+ this.selectedIndex = this.resultViews.length - 1;
+ }
+ this.changeFocus();
+ }
+
+ shiftFocusDown(event: KeyboardEvent) {
+ event.preventDefault();
+ if (this.selectedIndex >= 0) {
+ this.selectedIndex++;
+ this.selectedIndex %= this.resultViews.length;
+ } else {
+ this.selectedIndex = 0;
+ }
+ this.changeFocus();
+ }
+
+ changeFocus() {
+ if (this.resultViews.length > 0) {
+ this.resultViews.toArray()[this.selectedIndex].nativeElement.focus();
+ }
+ }
+
+ onKeydown(event: KeyboardEvent) {
+ if (event.key !== 'Enter') {
+ this.queryInput.nativeElement.focus();
+ }
+ }
+
+ close() {
+ this.show.next(false);
+ }
+
+ isNotEmpty(data) {
+ return isNotEmpty(data);
+ }
+
+ onClickSuggestion(data) {
+ this.clickSuggestion.emit(data);
+ return false;
+ }
+
+}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 79ddd8680f..2e8b40e5a5 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -40,14 +40,16 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
import { VarDirective } from './utils/var.directive';
-import { NotificationComponent } from './notifications/notification/notification.component';
-import { NotificationsBoardComponent } from './notifications/notifications-board/notifications-board.component';
import { DragClickDirective } from './utils/drag-click.directive';
import { TruncatePipe } from './utils/truncate.pipe';
import { TruncatableComponent } from './truncatable/truncatable.component';
import { TruncatableService } from './truncatable/truncatable.service';
import { TruncatablePartComponent } from './truncatable/truncatable-part/truncatable-part.component';
import { MockAdminGuard } from './mocks/mock-admin-guard.service';
+import { DebounceDirective } from './utils/debounce.directive';
+import { ClickOutsideDirective } from './utils/click-outside.directive';
+import { EmphasizePipe } from './utils/emphasize.pipe';
+import { InputSuggestionsComponent } from './input-suggestions/input-suggestions.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -65,7 +67,8 @@ const PIPES = [
EnumKeysPipe,
FileSizePipe,
SafeUrlPipe,
- TruncatePipe
+ TruncatePipe,
+ EmphasizePipe
];
const COMPONENTS = [
@@ -89,6 +92,7 @@ const COMPONENTS = [
ViewModeSwitchComponent,
TruncatableComponent,
TruncatablePartComponent,
+ InputSuggestionsComponent
];
const ENTRY_COMPONENTS = [
@@ -110,7 +114,9 @@ const PROVIDERS = [
const DIRECTIVES = [
VarDirective,
- DragClickDirective
+ DragClickDirective,
+ DebounceDirective,
+ ClickOutsideDirective
];
@NgModule({
diff --git a/src/app/shared/utils/click-outside.directive.ts b/src/app/shared/utils/click-outside.directive.ts
new file mode 100644
index 0000000000..2c759fe8fa
--- /dev/null
+++ b/src/app/shared/utils/click-outside.directive.ts
@@ -0,0 +1,20 @@
+import {Directive, ElementRef, Output, EventEmitter, HostListener} from '@angular/core';
+
+@Directive({
+ selector: '[dsClickOutside]'
+})
+export class ClickOutsideDirective {
+ constructor(private _elementRef: ElementRef) {
+ }
+
+ @Output()
+ public dsClickOutside = new EventEmitter();
+
+ @HostListener('document:click', ['$event.target'])
+ public onClick(targetElement) {
+ const clickedInside = this._elementRef.nativeElement.contains(targetElement);
+ if (!clickedInside) {
+ this.dsClickOutside.emit(null);
+ }
+ }
+}
diff --git a/src/app/shared/utils/debounce.directive.ts b/src/app/shared/utils/debounce.directive.ts
new file mode 100644
index 0000000000..d095bfcd24
--- /dev/null
+++ b/src/app/shared/utils/debounce.directive.ts
@@ -0,0 +1,43 @@
+import { Directive, Input, Output, EventEmitter, OnDestroy, OnInit } from '@angular/core';
+import { NgControl } from '@angular/forms';
+import 'rxjs/add/operator/debounceTime';
+import 'rxjs/add/operator/distinctUntilChanged';
+import 'rxjs/add/operator/takeUntil';
+import { Subject } from 'rxjs/Subject';
+
+@Directive({
+ selector: '[ngModel][dsDebounce]',
+})
+export class DebounceDirective implements OnInit, OnDestroy {
+ @Output()
+ public onDebounce = new EventEmitter();
+
+ @Input('dsDebounce')
+ public debounceTime = 500;
+
+ private isFirstChange = true;
+ private ngUnsubscribe: Subject = new Subject();
+
+ constructor(public model: NgControl) {
+ }
+
+ ngOnInit() {
+ this.model.valueChanges
+ .takeUntil(this.ngUnsubscribe)
+ .debounceTime(this.debounceTime)
+ .distinctUntilChanged()
+ .subscribe((modelValue) => {
+ if (this.isFirstChange) {
+ this.isFirstChange = false;
+ } else {
+ this.onDebounce.emit(modelValue);
+ }
+ });
+ }
+
+ ngOnDestroy() {
+ this.ngUnsubscribe.next();
+ this.ngUnsubscribe.complete();
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/shared/utils/emphasize.pipe.ts b/src/app/shared/utils/emphasize.pipe.ts
new file mode 100644
index 0000000000..506974c569
--- /dev/null
+++ b/src/app/shared/utils/emphasize.pipe.ts
@@ -0,0 +1,36 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({ name: 'dsEmphasize' })
+export class EmphasizePipe implements PipeTransform {
+ specials = [
+ // order matters for these
+ '-'
+ , '['
+ , ']'
+ // order doesn't matter for any of these
+ , '/'
+ , '{'
+ , '}'
+ , '('
+ , ')'
+ , '*'
+ , '+'
+ , '?'
+ , '.'
+ , '\\'
+ , '^'
+ , '$'
+ , '|'
+ ];
+ regex = RegExp('[' + this.specials.join('\\') + ']', 'g');
+
+ transform(haystack, needle): any {
+ const escaped = this.escapeRegExp(needle);
+ const reg = new RegExp(escaped, 'gi');
+ return haystack.replace(reg, '$&');
+ }
+
+ escapeRegExp(str) {
+ return str.replace(this.regex, '\\$&');
+ }
+}