mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-17 15:03:07 +00:00
Fixed issue #1 and some other fixes
This commit is contained in:
@@ -2,7 +2,6 @@ import 'rxjs/add/observable/of';
|
||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
import { ViewMode } from './search-options.model';
|
||||
|
||||
describe('PaginatedSearchOptions', () => {
|
||||
let options: PaginatedSearchOptions;
|
||||
@@ -19,7 +18,6 @@ describe('PaginatedSearchOptions', () => {
|
||||
options.filters = filters;
|
||||
options.query = query;
|
||||
options.scope = scope;
|
||||
options.view = ViewMode.Grid;
|
||||
});
|
||||
|
||||
describe('when toRestUrl is called', () => {
|
||||
|
@@ -91,8 +91,6 @@ describe('SearchFacetFilterComponent', () => {
|
||||
fixture = TestBed.createComponent(SearchFacetFilterComponent);
|
||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||
comp.filterConfig = mockFilterConfig;
|
||||
comp.filterValues = [mockValues];
|
||||
// comp.filterValues$ = new BehaviorSubject({});
|
||||
filterService = (comp as any).filterService;
|
||||
searchService = (comp as any).searchService;
|
||||
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues);
|
||||
@@ -198,10 +196,9 @@ describe('SearchFacetFilterComponent', () => {
|
||||
});
|
||||
|
||||
describe('when updateFilterValueList is called', () => {
|
||||
const searchOptions = new SearchOptions();
|
||||
beforeEach(() => {
|
||||
spyOn(comp, 'showFirstPageOnly');
|
||||
comp.updateFilterValueList(searchOptions)
|
||||
comp.updateFilterValueList()
|
||||
});
|
||||
|
||||
it('should call showFirstPageOnly and empty the filter', () => {
|
||||
@@ -211,7 +208,6 @@ describe('SearchFacetFilterComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('when findSuggestions is called with query \'test\'', () => {
|
||||
const query = 'test';
|
||||
beforeEach(() => {
|
||||
|
@@ -189,6 +189,9 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
this.filter = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* For usage of the hasValue function in the template
|
||||
*/
|
||||
hasValue(o: any): boolean {
|
||||
return hasValue(o);
|
||||
}
|
||||
|
@@ -248,7 +248,7 @@ export class SearchFilterService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a expand action to the store for a given filter
|
||||
* Dispatches an expand action to the store for a given filter
|
||||
* @param {string} filterName The filter for which the action is dispatched
|
||||
*/
|
||||
public expand(filterName: string): void {
|
||||
@@ -264,7 +264,7 @@ export class SearchFilterService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a initial collapse action to the store for a given filter
|
||||
* Dispatches an initial collapse action to the store for a given filter
|
||||
* @param {string} filterName The filter for which the action is dispatched
|
||||
*/
|
||||
public initialCollapse(filterName: string): void {
|
||||
@@ -272,7 +272,7 @@ export class SearchFilterService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a initial expand action to the store for a given filter
|
||||
* Dispatches an initial expand action to the store for a given filter
|
||||
* @param {string} filterName The filter for which the action is dispatched
|
||||
*/
|
||||
public initialExpand(filterName: string): void {
|
||||
@@ -288,7 +288,7 @@ export class SearchFilterService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a increment page action to the store for a given filter
|
||||
* Dispatches an increment page action to the store for a given filter
|
||||
* @param {string} filterName The filter for which the action is dispatched
|
||||
*/
|
||||
public incrementPage(filterName: string): void {
|
||||
|
@@ -96,7 +96,6 @@ describe('SearchRangeFilterComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchRangeFilterComponent);
|
||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||
comp.filterValues = [mockValues];
|
||||
filterService = (comp as any).filterService;
|
||||
searchService = (comp as any).searchService;
|
||||
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues);
|
||||
@@ -104,9 +103,9 @@ describe('SearchRangeFilterComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the getAddParams method is called wih a value', () => {
|
||||
describe('when the getChangeParams method is called wih a value', () => {
|
||||
it('should return the selectedValue list with the new parameter value', () => {
|
||||
const result$ = comp.getAddParams(value3);
|
||||
const result$ = comp.getChangeParams(value3);
|
||||
result$.subscribe((result) => {
|
||||
expect(result[mockFilterConfig.paramName + minSuffix]).toEqual(['1990']);
|
||||
expect(result[mockFilterConfig.paramName + maxSuffix]).toEqual(['1992']);
|
||||
@@ -114,16 +113,6 @@ describe('SearchRangeFilterComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the getRemoveParams method is called wih a value', () => {
|
||||
it('should return the selectedValue list with the parameter value left out', () => {
|
||||
const result$ = comp.getRemoveParams(value1);
|
||||
result$.subscribe((result) => {
|
||||
expect(result[mockFilterConfig.paramName + minSuffix]).toBeNull();
|
||||
expect(result[mockFilterConfig.paramName + maxSuffix]).toBeNull();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the onSubmit method is called with data', () => {
|
||||
const searchUrl = '/search/path';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import 'rxjs/add/observable/of';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
import { SearchOptions, ViewMode } from './search-options.model';
|
||||
import { SearchOptions } from './search-options.model';
|
||||
|
||||
describe('SearchOptions', () => {
|
||||
let options: PaginatedSearchOptions;
|
||||
@@ -13,7 +13,6 @@ describe('SearchOptions', () => {
|
||||
options.filters = filters;
|
||||
options.query = query;
|
||||
options.scope = scope;
|
||||
options.view = ViewMode.Grid;
|
||||
});
|
||||
|
||||
describe('when toRestUrl is called', () => {
|
||||
|
@@ -2,20 +2,10 @@ import { isNotEmpty } from '../shared/empty.util';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import 'core-js/library/fn/object/entries';
|
||||
|
||||
/**
|
||||
* This enumeration represents all possible ways of representing the a group of objects
|
||||
*/
|
||||
|
||||
export enum ViewMode {
|
||||
List = 'list',
|
||||
Grid = 'grid'
|
||||
}
|
||||
|
||||
/**
|
||||
* This model class represents all parameters needed to request information about a certain search request
|
||||
*/
|
||||
export class SearchOptions {
|
||||
view?: ViewMode = ViewMode.List;
|
||||
scope?: string;
|
||||
query?: string;
|
||||
filters?: any;
|
||||
|
@@ -31,7 +31,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||
[searchConfig]="searchOptions$ | async" [sortConfig]="sortConfig"></ds-search-results>
|
||||
[searchConfig]="searchOptions$ | async"></ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -12,9 +12,9 @@ import { SearchFilterService } from './search-filters/search-filter/search-filte
|
||||
import { SearchResult } from './search-result.model';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
@@ -38,13 +38,12 @@ export class SearchPageComponent implements OnInit {
|
||||
/**
|
||||
* The current search results
|
||||
*/
|
||||
resultsRD$: Subject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new Subject();
|
||||
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* The current paginated search options
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
sortConfig: SortOptions;
|
||||
|
||||
/**
|
||||
* The current relevant scopes
|
||||
@@ -91,8 +90,7 @@ export class SearchPageComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.searchOptions$ = this.filterService.getPaginatedSearchOptions(this.defaults);
|
||||
this.sub = this.searchOptions$.subscribe((searchOptions) =>
|
||||
this.service.search(searchOptions).first().subscribe((results) => this.resultsRD$.next(results))
|
||||
);
|
||||
this.service.search(searchOptions).filter((rd) => !rd.isLoading).first().subscribe((results) => this.resultsRD$.next(results)));
|
||||
|
||||
this.scopeListRD$ = this.filterService.getCurrentScope().pipe(
|
||||
flatMap((scopeId) => this.service.getScopes(scopeId))
|
||||
@@ -107,7 +105,7 @@ export class SearchPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to a expanded state
|
||||
* Set the sidebar to an expanded state
|
||||
*/
|
||||
public openSidebar(): void {
|
||||
this.sidebarService.expand();
|
||||
|
@@ -82,5 +82,9 @@ const effects = [
|
||||
SearchBooleanFilterComponent,
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* This module handles all components and pipes that are necessary for the search page
|
||||
*/
|
||||
export class SearchPageModule {
|
||||
}
|
||||
|
@@ -2,10 +2,10 @@ import { Component, Input } from '@angular/core';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||
import { SearchOptions, ViewMode } from '../search-options.model';
|
||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { SearchOptions } from '../search-options.model';
|
||||
import { SearchResult } from '../search-result.model';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-results',
|
||||
@@ -30,13 +30,9 @@ export class SearchResultsComponent {
|
||||
*/
|
||||
@Input() searchConfig: SearchOptions;
|
||||
|
||||
/**
|
||||
* The current sort options for the search
|
||||
*/
|
||||
@Input() sortConfig: SortOptions;
|
||||
|
||||
/**
|
||||
* The current view mode for the search results
|
||||
*/
|
||||
@Input() viewMode: ViewMode;
|
||||
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@ import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { SearchService } from './search.service';
|
||||
import { ViewMode } from '../../+search-page/search-options.model';
|
||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
@@ -28,6 +27,7 @@ import { SearchQueryResponse } from './search-query-response.model';
|
||||
import { SearchFilterConfig } from './search-filter-config.model';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
|
||||
@Component({ template: '' })
|
||||
class DummyComponent {
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute, NavigationExtras, Params, PRIMARY_OUTLET, Router,
|
||||
ActivatedRoute,
|
||||
NavigationExtras,
|
||||
PRIMARY_OUTLET,
|
||||
Router,
|
||||
UrlSegmentGroup
|
||||
} from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { filter, flatMap, map, tap } from 'rxjs/operators';
|
||||
import { ViewMode } from '../../+search-page/search-options.model';
|
||||
import { flatMap, map } from 'rxjs/operators';
|
||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import {
|
||||
FacetConfigSuccessResponse,
|
||||
FacetValueSuccessResponse,
|
||||
@@ -25,8 +26,7 @@ import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||
import { configureRequest } from '../../core/shared/operators';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
||||
import { SearchOptions } from '../search-options.model';
|
||||
import { SearchResult } from '../search-result.model';
|
||||
@@ -44,6 +44,7 @@ import { Community } from '../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
|
||||
|
||||
/**
|
||||
|
@@ -13,6 +13,7 @@ import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SearchFilterService } from '../search-filters/search-filter/search-filter.service';
|
||||
import { hot } from 'jasmine-marbles';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
|
||||
describe('SearchSettingsComponent', () => {
|
||||
|
||||
@@ -56,7 +57,7 @@ describe('SearchSettingsComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
declarations: [SearchSettingsComponent, EnumKeysPipe],
|
||||
declarations: [SearchSettingsComponent, EnumKeysPipe, VarDirective],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: searchServiceStub },
|
||||
|
||||
@@ -96,15 +97,18 @@ describe('SearchSettingsComponent', () => {
|
||||
});
|
||||
|
||||
it('it should show the order settings with the respective selectable options', () => {
|
||||
(comp as any).searchOptions$.first().subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||
expect(orderSetting).toBeDefined();
|
||||
const childElements = orderSetting.query(By.css('.form-control')).children;
|
||||
expect(childElements.length).toEqual(comp.searchOptionPossibilities.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('it should show the size settings with the respective selectable options', () => {
|
||||
(comp as any).filterService.getPaginatedSearchOptions().first().subscribe((options) => {
|
||||
fixture.detectChanges()
|
||||
(comp as any).searchOptions$.first().subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
|
||||
expect(pageSizeSetting).toBeDefined();
|
||||
const childElements = pageSizeSetting.query(By.css('.form-control')).children;
|
||||
@@ -114,15 +118,21 @@ describe('SearchSettingsComponent', () => {
|
||||
});
|
||||
|
||||
it('should have the proper order value selected by default', () => {
|
||||
(comp as any).searchOptions$.first().subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||
const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]'))
|
||||
const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]'));
|
||||
expect(childElementToBeSelected).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have the proper rpp value selected by default', () => {
|
||||
(comp as any).searchOptions$.first().subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
|
||||
const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]'))
|
||||
const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]'));
|
||||
expect(childElementToBeSelected).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -18,7 +18,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
export class SearchSidebarComponent {
|
||||
|
||||
/**
|
||||
* The total amount of result
|
||||
* The total amount of results
|
||||
*/
|
||||
@Input() resultCount;
|
||||
|
||||
|
@@ -31,7 +31,7 @@ export class SearchSidebarService {
|
||||
|
||||
/**
|
||||
* Checks if the sidebar should currently be collapsed
|
||||
* @returns {Observable<boolean>} Emits true if our screen size is mobile or when the state in the store is currently collapsed
|
||||
* @returns {Observable<boolean>} Emits true if the user's screen size is mobile or when the state in the store is currently collapsed
|
||||
*/
|
||||
get isCollapsed(): Observable<boolean> {
|
||||
return Observable.combineLatest(
|
||||
@@ -48,7 +48,7 @@ export class SearchSidebarService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a expand action to the store
|
||||
* Dispatches an expand action to the store
|
||||
*/
|
||||
public expand(): void {
|
||||
this.store.dispatch(new SearchSidebarExpandAction());
|
||||
|
8
src/app/core/shared/view-mode.model.ts
Normal file
8
src/app/core/shared/view-mode.model.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* This enumeration represents all possible ways of representing a group of objects in the UI
|
||||
*/
|
||||
|
||||
export enum ViewMode {
|
||||
List = 'list',
|
||||
Grid = 'grid'
|
||||
}
|
@@ -9,13 +9,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import { hasValue, isNotEmpty } from '../empty.util';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* 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',
|
||||
@@ -23,28 +16,92 @@ import { ActivatedRoute } from '@angular/router';
|
||||
templateUrl: './input-suggestions.component.html'
|
||||
})
|
||||
|
||||
/**
|
||||
* Component representing a form with a autocomplete functionality
|
||||
*/
|
||||
export class InputSuggestionsComponent {
|
||||
/**
|
||||
* The suggestions that should be shown
|
||||
*/
|
||||
@Input() suggestions: any[] = [];
|
||||
|
||||
/**
|
||||
* The time waited to detect if any other input will follow before requesting the suggestions
|
||||
*/
|
||||
@Input() debounceTime = 500;
|
||||
|
||||
/**
|
||||
* Placeholder attribute for the input field
|
||||
*/
|
||||
@Input() placeholder = '';
|
||||
|
||||
/**
|
||||
* Action attribute for the form
|
||||
*/
|
||||
@Input() action;
|
||||
|
||||
/**
|
||||
* Name attribute for the input field
|
||||
*/
|
||||
@Input() name;
|
||||
|
||||
/**
|
||||
* Value of the input field
|
||||
*/
|
||||
@Input() ngModel;
|
||||
|
||||
/**
|
||||
* Output for when the input field's value changes
|
||||
*/
|
||||
@Output() ngModelChange = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Output for when the form is submitted
|
||||
*/
|
||||
@Output() submitSuggestion = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Output for when a suggestion is clicked
|
||||
*/
|
||||
@Output() clickSuggestion = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Output for when new suggestions should be requested
|
||||
*/
|
||||
@Output() findSuggestions = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Emits true when the list of suggestions should be shown
|
||||
*/
|
||||
show = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* Index of the currently selected suggestion
|
||||
*/
|
||||
selectedIndex = -1;
|
||||
|
||||
/**
|
||||
* Reference to the input field component
|
||||
*/
|
||||
@ViewChild('inputField') queryInput: ElementRef;
|
||||
/**
|
||||
* Reference to the suggestion components
|
||||
*/
|
||||
@ViewChildren('suggestion') resultViews: QueryList<ElementRef>;
|
||||
|
||||
/**
|
||||
* When any of the inputs change, check if we should still show the suggestions
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (hasValue(changes.suggestions)) {
|
||||
this.show.next(isNotEmpty(changes.suggestions.currentValue));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the focus on one of the suggestions up to the previous suggestion
|
||||
* When no suggestion is currently in focus OR the first suggestion is in focus: shift to the last suggestion
|
||||
*/
|
||||
shiftFocusUp(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
if (this.selectedIndex > 0) {
|
||||
@@ -56,6 +113,10 @@ export class InputSuggestionsComponent {
|
||||
this.changeFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the focus on one of the suggestions up to the next suggestion
|
||||
* When no suggestion is currently in focus OR the last suggestion is in focus: shift to the first suggestion
|
||||
*/
|
||||
shiftFocusDown(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
if (this.selectedIndex >= 0) {
|
||||
@@ -67,26 +128,42 @@ export class InputSuggestionsComponent {
|
||||
this.changeFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the change of focus to the current selectedIndex
|
||||
*/
|
||||
changeFocus() {
|
||||
if (this.resultViews.length > 0) {
|
||||
this.resultViews.toArray()[this.selectedIndex].nativeElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When any key is pressed (except for the Enter button) the query input should move to the input field
|
||||
* @param {KeyboardEvent} event The keyboard event
|
||||
*/
|
||||
onKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== 'Enter') {
|
||||
this.queryInput.nativeElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the show variable so the suggestion dropdown closes
|
||||
*/
|
||||
close() {
|
||||
this.show.next(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* For usage of the isNotEmpty function in the template
|
||||
*/
|
||||
isNotEmpty(data) {
|
||||
return isNotEmpty(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that if a suggestion is clicked, the suggestions dropdown closes and the focus moves to the input field
|
||||
*/
|
||||
onClickSuggestion(data) {
|
||||
this.clickSuggestion.emit(data);
|
||||
this.close();
|
||||
|
@@ -1,13 +1,11 @@
|
||||
import { ObjectCollectionComponent } from './object-collection.component';
|
||||
import { ViewMode } from '../../+search-page/search-options.model';
|
||||
import { element } from 'protractor';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Config } from '../../../config/config.interface';
|
||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { RouterStub } from '../testing/router-stub';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
|
||||
describe('ObjectCollectionComponent', () => {
|
||||
let fixture: ComponentFixture<ObjectCollectionComponent>;
|
||||
|
@@ -14,8 +14,8 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
|
||||
import { ListableObject } from './shared/listable-object.model';
|
||||
import { ViewMode } from '../../+search-page/search-options.model';
|
||||
import { hasValue, isNotEmpty } from '../empty.util';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-viewable-collection',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ViewMode } from '../../../+search-page/search-options.model';
|
||||
import { renderElementsFor } from './dso-element-decorator';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
describe('ElementDecorator', () => {
|
||||
const gridDecorator = renderElementsFor(Item, ViewMode.Grid);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||
import { ListableObject } from './listable-object.model';
|
||||
import { ViewMode } from '../../../+search-page/search-options.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
const dsoElementMap = new Map();
|
||||
export function renderElementsFor(listable: GenericConstructor<ListableObject>, viewMode: ViewMode) {
|
||||
|
@@ -2,8 +2,8 @@ import { Component, Inject } from '@angular/core';
|
||||
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator';
|
||||
import { ViewMode } from '../../../+search-page/search-options.model';
|
||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-grid-element',
|
||||
|
@@ -3,7 +3,7 @@ import { Component, Input, Inject } from '@angular/core';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||
import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator';
|
||||
import { ViewMode } from '../../../+search-page/search-options.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-grid-element',
|
||||
|
@@ -3,7 +3,7 @@ import { Component, Input, Inject } from '@angular/core';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator';
|
||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||
import { ViewMode } from '../../../+search-page/search-options.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-grid-element',
|
||||
|
@@ -3,8 +3,8 @@ import { Component } from '@angular/core';
|
||||
import { renderElementsFor} from '../../../object-collection/shared/dso-element-decorator';
|
||||
import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-search-result-grid-element',
|
||||
|
@@ -2,8 +2,8 @@ import { Component } from '@angular/core';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
|
||||
import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-search-result-grid-element',
|
||||
|
@@ -4,8 +4,8 @@ import { renderElementsFor } from '../../../object-collection/shared/dso-element
|
||||
import { SearchResultGridElementComponent } from '../search-result-grid-element.component';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
import { focusShadow } from '../../../../shared/animations/focus';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-search-result-grid-element',
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { Component, Injector, Input, OnInit } from '@angular/core';
|
||||
import { ViewMode } from '../../../+search-page/search-options.model';
|
||||
import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||
import { rendersDSOType } from '../../object-collection/shared/dso-element-decorator';
|
||||
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-wrapper-grid-element',
|
||||
|
@@ -2,8 +2,8 @@ import { Component, Inject } from '@angular/core';
|
||||
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator';
|
||||
import { ViewMode } from '../../../+search-page/search-options.model';
|
||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-list-element',
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Component, Input, Inject } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||
import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator';
|
||||
import { ViewMode } from '../../../+search-page/search-options.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-list-element',
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Component, Input, Inject } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||
import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator';
|
||||
import { ViewMode } from '../../../+search-page/search-options.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-list-element',
|
||||
|
@@ -4,8 +4,8 @@ import { renderElementsFor } from '../../../object-collection/shared/dso-element
|
||||
|
||||
import { SearchResultListElementComponent } from '../search-result-list-element.component';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-search-result-list-element',
|
||||
|
@@ -4,8 +4,8 @@ import { renderElementsFor } from '../../../object-collection/shared/dso-element
|
||||
|
||||
import { SearchResultListElementComponent } from '../search-result-list-element.component';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-search-result-list-element',
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator';
|
||||
import { SearchResultListElementComponent } from '../search-result-list-element.component';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
|
||||
import { ViewMode } from '../../../../+search-page/search-options.model';
|
||||
import { ListableObject } from '../../../object-collection/shared/listable-object.model';
|
||||
import { focusBackground } from '../../../animations/focus';
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-search-result-list-element',
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { Component, Injector, Input, OnInit } from '@angular/core';
|
||||
import { ViewMode } from '../../../+search-page/search-options.model';
|
||||
import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||
import { rendersDSOType } from '../../object-collection/shared/dso-element-decorator'
|
||||
import { ListableObject } from '../../object-collection/shared/listable-object.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-wrapper-list-element',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="row" action="/search">
|
||||
<div *ngIf="isNotEmpty(scopes)" class="col-12 col-sm-3">
|
||||
<select [(ngModel)]="selectedId" name="scope" class="form-control" aria-label="Search scope" (change)="onScopeChange($event.target.value)">
|
||||
<select [(ngModel)]="scope" name="scope" class="form-control" aria-label="Search scope" (change)="onScopeChange($event.target.value)">
|
||||
<option value>{{'search.form.search_dspace' | translate}}</option>
|
||||
<option *ngFor="let scopeOption of scopes" [value]="scopeOption.id">{{scopeOption?.name ? scopeOption.name : 'search.form.search_dspace' | translate}}</option>
|
||||
</select>
|
||||
|
@@ -3,6 +3,7 @@ import { SearchService } from '../../+search-page/search-service/search.service'
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { Router } from '@angular/router';
|
||||
import { isNotEmpty, hasValue, isEmpty, hasNoValue } from '../empty.util';
|
||||
import { QueryParamsHandling } from '@angular/router/src/config';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
@@ -15,36 +16,66 @@ import { isNotEmpty, hasValue, isEmpty, hasNoValue } from '../empty.util';
|
||||
styleUrls: ['./search-form.component.scss'],
|
||||
templateUrl: './search-form.component.html'
|
||||
})
|
||||
export class SearchFormComponent {
|
||||
@Input() query: string;
|
||||
selectedId = '';
|
||||
@Input() currentUrl: string;
|
||||
@Input() scopes: DSpaceObject[];
|
||||
|
||||
/**
|
||||
* Component that represents the search form
|
||||
*/
|
||||
export class SearchFormComponent {
|
||||
/**
|
||||
* The search query
|
||||
*/
|
||||
@Input() query: string;
|
||||
|
||||
/**
|
||||
* The currently selected scope object's UUID
|
||||
*/
|
||||
@Input()
|
||||
set scope(id: string) {
|
||||
this.selectedId = id;
|
||||
}
|
||||
scope = '';
|
||||
@Input() currentUrl: string;
|
||||
|
||||
/**
|
||||
* The available scopes
|
||||
*/
|
||||
@Input() scopes: DSpaceObject[];
|
||||
|
||||
constructor(private router: Router) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the search when the form is submitted
|
||||
* @param data Values submitted using the form
|
||||
*/
|
||||
onSubmit(data: any) {
|
||||
this.updateSearch(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the search when the current scope has been changed
|
||||
* @param {string} scope The new scope
|
||||
*/
|
||||
onScopeChange(scope: string) {
|
||||
this.updateSearch({ scope });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the search URL
|
||||
* @param data Updated parameters
|
||||
*/
|
||||
updateSearch(data: any) {
|
||||
const newUrl = hasValue(this.currentUrl) ? this.currentUrl : 'search';
|
||||
const newUrl = hasValue(this.currentUrl) ? this.currentUrl : '/search';
|
||||
let handling: QueryParamsHandling = '' ;
|
||||
if (this.currentUrl === '/search') {
|
||||
handling = 'merge';
|
||||
}
|
||||
this.router.navigate([newUrl], {
|
||||
queryParams: Object.assign({}, { page: 1 }, data),
|
||||
queryParamsHandling: 'merge'
|
||||
queryParamsHandling: handling
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* For usage of the isNotEmpty function in the template
|
||||
*/
|
||||
isNotEmpty(object: any) {
|
||||
return isNotEmpty(object);
|
||||
}
|
||||
|
@@ -156,6 +156,10 @@ const DIRECTIVES = [
|
||||
...ENTRY_COMPONENTS
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* This module handles all components and pipes that need to be shared among multiple other modules
|
||||
*/
|
||||
export class SharedModule {
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,4 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ViewMode } from '../../+search-page/search-options.model';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
|
||||
export class HALEndpointServiceStub {
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ViewMode } from '../../+search-page/search-options.model';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
|
||||
export class SearchServiceStub {
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { TruncatableService } from '../truncatable.service';
|
||||
import { hasValue } from '../../empty.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-truncatable-part',
|
||||
@@ -7,22 +8,59 @@ import { TruncatableService } from '../truncatable.service';
|
||||
styleUrls: ['./truncatable-part.component.scss']
|
||||
})
|
||||
|
||||
/**
|
||||
* Component that truncates/clamps a piece of text
|
||||
* It needs a TruncatableComponent parent to identify it's current state
|
||||
*/
|
||||
export class TruncatablePartComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Number of lines shown when the part is collapsed
|
||||
*/
|
||||
@Input() minLines: number;
|
||||
|
||||
/**
|
||||
* Number of lines shown when the part is expanded. -1 indicates no limit
|
||||
*/
|
||||
@Input() maxLines = -1;
|
||||
|
||||
/**
|
||||
* The identifier of the parent TruncatableComponent
|
||||
*/
|
||||
@Input() id: string;
|
||||
|
||||
/**
|
||||
* Type of text, can be a h4 for headers or any other class you want to add
|
||||
*/
|
||||
@Input() type: string;
|
||||
|
||||
/**
|
||||
* True if the minimal height of the part should at least be as high as it's minimum amount of lines
|
||||
*/
|
||||
@Input() fixedHeight = false;
|
||||
|
||||
/**
|
||||
* Current amount of lines shown of this part
|
||||
*/
|
||||
lines: string;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from
|
||||
*/
|
||||
private sub;
|
||||
|
||||
public constructor(private service: TruncatableService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize lines variable
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.setLines();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the current state to determine how much lines should be shown of this part
|
||||
*/
|
||||
private setLines() {
|
||||
this.sub = this.service.isCollapsed(this.id).subscribe((collapsed: boolean) => {
|
||||
if (collapsed) {
|
||||
@@ -33,7 +71,12 @@ export class TruncatablePartComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,22 +16,43 @@ export const TruncatableActionTypes = {
|
||||
};
|
||||
|
||||
export class TruncatableAction implements Action {
|
||||
/**
|
||||
* UUID of the truncatable component the action is performed on, used to identify the filter
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Type of action that will be performed
|
||||
*/
|
||||
type;
|
||||
constructor(name: string) {
|
||||
this.id = name;
|
||||
|
||||
/**
|
||||
* Initialize with the truncatable component's UUID
|
||||
* @param {string} id of the filter
|
||||
*/
|
||||
constructor(id: string) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to collapse a truncatable component when it's expanded and expand it when it's collapsed
|
||||
*/
|
||||
export class TruncatableToggleAction extends TruncatableAction {
|
||||
type = TruncatableActionTypes.TOGGLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to collapse a truncatable component
|
||||
*/
|
||||
export class TruncatableCollapseAction extends TruncatableAction {
|
||||
type = TruncatableActionTypes.COLLAPSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to expand a truncatable component
|
||||
*/
|
||||
export class TruncatableExpandAction extends TruncatableAction {
|
||||
type = TruncatableActionTypes.EXPAND;
|
||||
}
|
||||
|
@@ -9,14 +9,32 @@ import { TruncatableService } from './truncatable.service';
|
||||
styleUrls: ['./truncatable.component.scss'],
|
||||
|
||||
})
|
||||
|
||||
/**
|
||||
* Component that represents a section with one or more truncatable parts that all listen to this state
|
||||
*/
|
||||
export class TruncatableComponent {
|
||||
/**
|
||||
* Is true when all truncatable parts in this truncatable should be expanded on loading
|
||||
*/
|
||||
@Input() initialExpand = false;
|
||||
|
||||
/**
|
||||
* The unique identifier of this truncatable component
|
||||
*/
|
||||
@Input() id: string;
|
||||
|
||||
/**
|
||||
* Is true when the truncatable should expand on both hover as click
|
||||
*/
|
||||
@Input() onHover = false;
|
||||
|
||||
public constructor(private service: TruncatableService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the initial state
|
||||
*/
|
||||
ngOnInit() {
|
||||
if (this.initialExpand) {
|
||||
this.service.expand(this.id);
|
||||
@@ -25,18 +43,27 @@ export class TruncatableComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If onHover is true, collapses the truncatable
|
||||
*/
|
||||
public hoverCollapse() {
|
||||
if (this.onHover) {
|
||||
this.service.collapse(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If onHover is true, expands the truncatable
|
||||
*/
|
||||
public hoverExpand() {
|
||||
if (this.onHover) {
|
||||
this.service.expand(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the truncatable when it's collapsed, collapses it when it's expanded
|
||||
*/
|
||||
public toggle() {
|
||||
this.service.toggle(this.id);
|
||||
}
|
||||
|
@@ -1,15 +1,27 @@
|
||||
import { TruncatableAction, TruncatableActionTypes } from './truncatable.actions';
|
||||
|
||||
/**
|
||||
* Interface that represents the state of a single truncatable
|
||||
*/
|
||||
export interface TruncatableState {
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface that represents the state of all truncatable
|
||||
*/
|
||||
export interface TruncatablesState {
|
||||
[id: string]: TruncatableState
|
||||
}
|
||||
|
||||
const initialState: TruncatablesState = Object.create(null);
|
||||
|
||||
/**
|
||||
* Performs a truncatable action on the current state
|
||||
* @param {TruncatablesState} state The state before the action is performed
|
||||
* @param {TruncatableAction} action The action that should be performed
|
||||
* @returns {TruncatablesState} The state after the action is performed
|
||||
*/
|
||||
export function truncatableReducer(state = initialState, action: TruncatableAction): TruncatablesState {
|
||||
|
||||
switch (action.type) {
|
||||
|
@@ -7,12 +7,20 @@ import { hasValue } from '../empty.util';
|
||||
|
||||
const truncatableStateSelector = (state: TruncatablesState) => state.truncatable;
|
||||
|
||||
/**
|
||||
* Service responsible for truncating/clamping text and performing actions on truncatable elements
|
||||
*/
|
||||
@Injectable()
|
||||
export class TruncatableService {
|
||||
|
||||
constructor(private store: Store<TruncatablesState>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a trunctable component should currently be collapsed
|
||||
* @param {string} id The UUID of the truncatable component
|
||||
* @returns {Observable<boolean>} Emits true if the state in the store is currently collapsed for the given truncatable component
|
||||
*/
|
||||
isCollapsed(id: string): Observable<boolean> {
|
||||
return this.store.select(truncatableByIdSelector(id))
|
||||
.map((object: TruncatableState) => {
|
||||
@@ -24,14 +32,26 @@ export class TruncatableService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a toggle action to the store for a given truncatable component
|
||||
* @param {string} id The identifier of the truncatable for which the action is dispatched
|
||||
*/
|
||||
public toggle(id: string): void {
|
||||
this.store.dispatch(new TruncatableToggleAction(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a collapse action to the store for a given truncatable component
|
||||
* @param {string} id The identifier of the truncatable for which the action is dispatched
|
||||
*/
|
||||
public collapse(id: string): void {
|
||||
this.store.dispatch(new TruncatableCollapseAction(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an expand action to the store for a given truncatable component
|
||||
* @param {string} id The identifier of the truncatable for which the action is dispatched
|
||||
*/
|
||||
public expand(id: string): void {
|
||||
this.store.dispatch(new TruncatableExpandAction(id));
|
||||
}
|
||||
|
@@ -1,14 +1,18 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
|
||||
/**
|
||||
* Pipe to truncate a value in Angular. (Take a substring, starting at 0)
|
||||
* Default value: 10
|
||||
*/
|
||||
@Pipe({
|
||||
name: 'dsCapitalize'
|
||||
})
|
||||
|
||||
/**
|
||||
* Pipe for capizalizing a string
|
||||
*/
|
||||
export class CapitalizePipe implements PipeTransform {
|
||||
transform(value: string, args: string[]): string {
|
||||
/**
|
||||
* @param {string} value String to be capitalized
|
||||
* @returns {string} Capitalized version of the input value
|
||||
*/
|
||||
transform(value: string): string {
|
||||
if (value) {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
@@ -3,7 +3,13 @@ import {Directive, ElementRef, Output, EventEmitter, HostListener} from '@angula
|
||||
@Directive({
|
||||
selector: '[dsClickOutside]'
|
||||
})
|
||||
/**
|
||||
* Directive to detect when the users clicks outside of the element the directive was put on
|
||||
*/
|
||||
export class ClickOutsideDirective {
|
||||
/**
|
||||
* Emits null when the user clicks outside of the element
|
||||
*/
|
||||
@Output()
|
||||
public dsClickOutside = new EventEmitter();
|
||||
|
||||
|
@@ -8,22 +8,45 @@ import { Subject } from 'rxjs/Subject';
|
||||
@Directive({
|
||||
selector: '[ngModel][dsDebounce]',
|
||||
})
|
||||
/**
|
||||
* Directive for setting a debounce time on an input field
|
||||
* It will emit the input field's value when no changes were made to this value in a given debounce time
|
||||
*/
|
||||
export class DebounceDirective implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Emits a value when nothing has changed in dsDebounce milliseconds
|
||||
*/
|
||||
@Output()
|
||||
public onDebounce = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* The debounce time in milliseconds
|
||||
*/
|
||||
@Input()
|
||||
public dsDebounce = 500;
|
||||
|
||||
/**
|
||||
* True if no changes have been made to the input field's value
|
||||
*/
|
||||
private isFirstChange = true;
|
||||
private ngUnsubscribe: Subject<void> = new Subject<void>();
|
||||
|
||||
|
||||
/**
|
||||
* Subject to unsubscribe from
|
||||
*/
|
||||
private subject: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(public model: NgControl) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening to changes of the input field's value changes
|
||||
* Emit it when the debounceTime is over without new changes
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.model.valueChanges
|
||||
.takeUntil(this.ngUnsubscribe)
|
||||
.takeUntil(this.subject)
|
||||
.debounceTime(this.dsDebounce)
|
||||
.distinctUntilChanged()
|
||||
.subscribe((modelValue) => {
|
||||
@@ -35,8 +58,11 @@ export class DebounceDirective implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close subject
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.ngUnsubscribe.next();
|
||||
this.ngUnsubscribe.complete();
|
||||
this.subject.next();
|
||||
this.subject.complete();
|
||||
}
|
||||
}
|
||||
|
@@ -3,15 +3,36 @@ import { Directive, EventEmitter, HostListener, Output } from '@angular/core';
|
||||
@Directive({
|
||||
selector: '[dsDragClick]'
|
||||
})
|
||||
|
||||
/**
|
||||
* Directive for preventing drag events being misinterpret as clicks
|
||||
* The difference is made using the time the mouse button is pushed down
|
||||
*/
|
||||
export class DragClickDirective {
|
||||
/**
|
||||
* The start time of the mouse down event in milliseconds
|
||||
*/
|
||||
private start;
|
||||
|
||||
/**
|
||||
* Emits a click event when the click is perceived as an actual click and not a drag
|
||||
*/
|
||||
@Output() actualClick = new EventEmitter();
|
||||
|
||||
/**
|
||||
* When the mouse button is pushed down, register the start time
|
||||
* @param event Mouse down event
|
||||
*/
|
||||
@HostListener('mousedown', ['$event'])
|
||||
mousedownEvent(event) {
|
||||
this.start = new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the mouse button is let go of, check how long if it was down for
|
||||
* If the mouse button was down for more than 250ms, don't emit a click event
|
||||
* @param event Mouse down event
|
||||
*/
|
||||
@HostListener('mouseup', ['$event'])
|
||||
mouseupEvent(event) {
|
||||
const end: any = new Date();
|
||||
|
@@ -1,7 +1,13 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({ name: 'dsEmphasize' })
|
||||
/**
|
||||
* Pipe for emphasizing a part of a string by surrounding it with <em> tags
|
||||
*/
|
||||
export class EmphasizePipe implements PipeTransform {
|
||||
/**
|
||||
* Characters that should be escaped
|
||||
*/
|
||||
specials = [
|
||||
// order matters for these
|
||||
'-'
|
||||
@@ -22,14 +28,28 @@ export class EmphasizePipe implements PipeTransform {
|
||||
, '$'
|
||||
, '|'
|
||||
];
|
||||
/**
|
||||
* Regular expression for escaping the string we're trying to find
|
||||
*/
|
||||
regex = RegExp('[' + this.specials.join('\\') + ']', 'g');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param haystack The string which we want to partly highlight
|
||||
* @param needle The string that should become emphasized in the haystack string
|
||||
* @returns {any} Transformed haystack with the needle emphasized
|
||||
*/
|
||||
transform(haystack, needle): any {
|
||||
const escaped = this.escapeRegExp(needle);
|
||||
const reg = new RegExp(escaped, 'gi');
|
||||
return haystack.replace(reg, '<em>$&</em>');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param str Escape special characters in the string we're looking for
|
||||
* @returns {any} The escaped version of the input string
|
||||
*/
|
||||
escapeRegExp(str) {
|
||||
return str.replace(this.regex, '\\$&');
|
||||
}
|
||||
|
@@ -1,8 +1,16 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({ name: 'dsKeys' })
|
||||
/**
|
||||
* Pipe for parsing all values of an enumeration to an array of key-value pairs
|
||||
*/
|
||||
export class EnumKeysPipe implements PipeTransform {
|
||||
transform(value, args: string[]): any {
|
||||
|
||||
/**
|
||||
* @param value An enumeration
|
||||
* @returns {any} Array with all keys and values of the input enumeration
|
||||
*/
|
||||
transform(value): any {
|
||||
const keys = [];
|
||||
for (const enumMember in value) {
|
||||
if (!isNaN(parseInt(enumMember, 10))) {
|
||||
|
@@ -1,7 +1,15 @@
|
||||
import { PipeTransform, Pipe } from '@angular/core';
|
||||
|
||||
@Pipe({name: 'dsObjectKeys'})
|
||||
/**
|
||||
* Pipe for parsing all keys of an object to an array of key-value pairs
|
||||
*/
|
||||
export class ObjectKeysPipe implements PipeTransform {
|
||||
|
||||
/**
|
||||
* @param value An object
|
||||
* @returns {any} Array with all keys the input object
|
||||
*/
|
||||
transform(value, args:string[]): any {
|
||||
const keys = [];
|
||||
Object.keys(value).forEach((k) => keys.push(k));
|
||||
|
@@ -8,8 +8,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
import { SearchService } from '../../+search-page/search-service/search.service';
|
||||
import { ViewModeSwitchComponent } from './view-mode-switch.component';
|
||||
import { ViewMode } from '../../+search-page/search-options.model';
|
||||
import { SearchServiceStub } from '../testing/search-service-stub';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
|
||||
@Component({ template: '' })
|
||||
class DummyComponent { }
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ViewMode } from '../../+search-page/search-options.model';
|
||||
import { SearchService } from './../../+search-page/search-service/search.service';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
|
||||
/**
|
||||
* Component to switch between list and grid views.
|
||||
|
Reference in New Issue
Block a user