diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index 5bd4f745d9..e842c0efd3 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -50,6 +50,7 @@ import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer'; import { correlationIdReducer } from './correlation-id/correlation-id.reducer'; +import { contextHelpReducer, ContextHelpState } from './shared/context-help.reducer'; export interface AppState { router: fromRouter.RouterReducerState; @@ -71,6 +72,7 @@ export interface AppState { epeopleRegistry: EPeopleRegistryState; groupRegistry: GroupRegistryState; correlationId: string; + contextHelp: ContextHelpState; } export const appReducers: ActionReducerMap = { @@ -92,7 +94,8 @@ export const appReducers: ActionReducerMap = { communityList: CommunityListReducer, epeopleRegistry: ePeopleRegistryReducer, groupRegistry: groupRegistryReducer, - correlationId: correlationIdReducer + correlationId: correlationIdReducer, + contextHelp: contextHelpReducer, }; export const routerStateSelector = (state: AppState) => state.router; diff --git a/src/app/page-internal-server-error/page-internal-server-error.component.html b/src/app/page-internal-server-error/page-internal-server-error.component.html index 595fd9961b..bb43cdc678 100644 --- a/src/app/page-internal-server-error/page-internal-server-error.component.html +++ b/src/app/page-internal-server-error/page-internal-server-error.component.html @@ -1,6 +1,6 @@

500

-

{{"500.page-internal-server-error" | translate}}

+

{{"500.page-internal-server-error" | translate}}


{{"500.help" | translate}}


diff --git a/src/app/shared/context-help-wrapper/context-help-wrapper.component.html b/src/app/shared/context-help-wrapper/context-help-wrapper.component.html index 939ad8323c..03fc46c172 100644 --- a/src/app/shared/context-help-wrapper/context-help-wrapper.component.html +++ b/src/app/shared/context-help-wrapper/context-help-wrapper.component.html @@ -10,12 +10,16 @@
- + #tooltip="ngbTooltip" + (click)="onClick()"> diff --git a/src/app/shared/context-help-wrapper/context-help-wrapper.component.ts b/src/app/shared/context-help-wrapper/context-help-wrapper.component.ts index 0ef535bf95..e4e22d7ebc 100644 --- a/src/app/shared/context-help-wrapper/context-help-wrapper.component.ts +++ b/src/app/shared/context-help-wrapper/context-help-wrapper.component.ts @@ -1,11 +1,16 @@ -import { Component, Input, OnInit, TemplateRef } from '@angular/core'; +import { Component, Input, OnInit, TemplateRef, OnDestroy, AfterViewInit, ViewChild } from '@angular/core'; import { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, of as observableOf } from 'rxjs'; +import { Observable, of as observableOf, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { PlacementDir } from './placement-dir.model'; +import content from '*.scss'; +import { ContextHelpService } from '../context-help.service'; +import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; +import { hasValueOperator } from '../empty.util'; +import { ContextHelp } from '../context-help.model'; -/* +/** * This component renders an info icon next to the wrapped element which * produces a tooltip when clicked. */ @@ -14,28 +19,39 @@ import { PlacementDir } from './placement-dir.model'; templateUrl: './context-help-wrapper.component.html', styleUrls: ['./context-help-wrapper.component.scss'], }) -export class ContextHelpWrapperComponent { - /* +export class ContextHelpWrapperComponent implements OnInit, AfterViewInit, OnDestroy { + /** * Template reference for the wrapped element. */ @Input() templateRef: TemplateRef; - /* + /** + * Identifier for the context help tooltip. + */ + @Input() id: string; + + /** * Indicate where the tooltip should show up, relative to the info icon. */ @Input() tooltipPlacement?: PlacementArray; - /* + /** * Indicate whether the info icon should appear to the left or to * the right of the wrapped element. */ @Input() iconPlacement?: PlacementDir; - /* + /** * If true, don't process text to render links. */ @Input() dontParseLinks?: boolean; + @ViewChild('tooltip', { static: false }) tooltip: NgbTooltip; + + shouldShowIcon$: Observable; + + private subs: Subscription[] = []; + // TODO: dependent on evaluation order of input setters? parsedContent$: Observable<(string | {href: string, text: string})[]> = observableOf([]); @Input() set content(content : string) { @@ -46,9 +62,48 @@ export class ContextHelpWrapperComponent { ); } - constructor(private translateService: TranslateService) { } + constructor( + private translateService: TranslateService, + private contextHelpService: ContextHelpService + ) { } - /* + ngOnInit() { + this.shouldShowIcon$ = this.contextHelpService.shouldShowIcons$(); + } + + ngAfterViewInit() { + this.subs = [ + this.contextHelpService.getContextHelp$(this.id) + .pipe(hasValueOperator()) + .subscribe((ch: ContextHelp) => { + if (ch.isTooltipVisible && !this.tooltip.isOpen()) { + this.tooltip.open(); + } else if (!ch.isTooltipVisible && this.tooltip.isOpen()) { + this.tooltip.close() + } + }), + + this.tooltip.shown.subscribe(() => { + this.contextHelpService.showTooltip(this.id); + }), + + this.tooltip.hidden.subscribe(() => { + this.contextHelpService.hideTooltip(this.id); + }) + ]; + } + + ngOnDestroy() { + for (let sub of this.subs) { + sub.unsubscribe(); + } + } + + onClick() { + this.contextHelpService.toggleTooltip(this.id); + } + + /** * Parses Markdown-style links, splitting up a given text * into link-free pieces of text and objects of the form * {href: string, text: string} (which represent links). diff --git a/src/app/shared/context-help.actions.ts b/src/app/shared/context-help.actions.ts index 2bd8b6a163..5f985c2f73 100644 --- a/src/app/shared/context-help.actions.ts +++ b/src/app/shared/context-help.actions.ts @@ -1,6 +1,6 @@ import { Action } from '@ngrx/store'; import { type } from './ngrx/type'; -import { ContextHelpModel } from './context-help.model'; +import { ContextHelp } from './context-help.model'; export const ContextHelpActionTypes = { 'CONTEXT_HELP_TOGGLE_ICONS': type('dspace/context-help/CONTEXT_HELP_TOGGLE_ICONS'), @@ -23,9 +23,9 @@ export class ContextHelpToggleIconsAction implements Action { */ export class ContextHelpAddAction implements Action { type = ContextHelpActionTypes.CONTEXT_HELP_ADD; - model: ContextHelpModel; + model: ContextHelp; - constructor (model: ContextHelpModel) { + constructor (model: ContextHelp) { this.model = model; } } diff --git a/src/app/shared/context-help.directive.ts b/src/app/shared/context-help.directive.ts index 1d8bcccfa7..89e283715a 100644 --- a/src/app/shared/context-help.directive.ts +++ b/src/app/shared/context-help.directive.ts @@ -1,15 +1,28 @@ -import { ComponentFactoryResolver, ComponentRef, Directive, ElementRef, Input, OnChanges, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; +import { + ComponentFactoryResolver, + ComponentRef, + Directive, + ElementRef, + Input, + OnChanges, + OnInit, + TemplateRef, + ViewContainerRef, + OnDestroy +} from '@angular/core'; import { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning'; import { ContextHelpWrapperComponent } from './context-help-wrapper/context-help-wrapper.component'; import { PlacementDir } from './context-help-wrapper/placement-dir.model'; +import { ContextHelpService } from './context-help.service'; export interface ContextHelpDirectiveInput { content: string; + id: string; tooltipPlacement?: PlacementArray; iconPlacement?: PlacementDir; } -/* +/** * Directive to add a clickable tooltip icon to an element. * The tooltip icon's position is configurable ('left' or 'right') * and so is the position of the tooltip itself (PlacementArray). @@ -17,14 +30,15 @@ export interface ContextHelpDirectiveInput { @Directive({ selector: '[dsContextHelp]', }) -export class ContextHelpDirective implements OnChanges { - /* +export class ContextHelpDirective implements OnChanges, OnDestroy { + /** * Expects an object with the following fields: * - content: a string referring to an entry in the i18n files * - tooltipPlacement: a PlacementArray describing where the tooltip should expand, relative to the tooltip icon * - iconPlacement: a string 'left' or 'right', describing where the tooltip icon should be placed, relative to the element */ - @Input() dsContextHelp: string | ContextHelpDirectiveInput; + @Input() dsContextHelp: ContextHelpDirectiveInput; + mostRecentId: string | undefined = undefined; protected wrapper: ComponentRef; @@ -32,19 +46,31 @@ export class ContextHelpDirective implements OnChanges { private templateRef: TemplateRef, private viewContainerRef: ViewContainerRef, private componentFactoryResolver: ComponentFactoryResolver, + private contextHelpService: ContextHelpService ){} ngOnChanges() { - const input: ContextHelpDirectiveInput = typeof(this.dsContextHelp) === 'string' - ? {content: this.dsContextHelp} - : this.dsContextHelp; + this.clearMostRecentId(); + this.mostRecentId = this.dsContextHelp.id; + this.contextHelpService.add({id: this.dsContextHelp.id}); // TODO: tooltipVisible field const factory = this.componentFactoryResolver.resolveComponentFactory(ContextHelpWrapperComponent); this.wrapper = this.viewContainerRef.createComponent(factory); this.wrapper.instance.templateRef = this.templateRef; - this.wrapper.instance.content = input.content; - this.wrapper.instance.tooltipPlacement = input.tooltipPlacement; - this.wrapper.instance.iconPlacement = input.iconPlacement; + this.wrapper.instance.content = this.dsContextHelp.content; + this.wrapper.instance.id = this.dsContextHelp.id; + this.wrapper.instance.tooltipPlacement = this.dsContextHelp.tooltipPlacement; + this.wrapper.instance.iconPlacement = this.dsContextHelp.iconPlacement; + } + + ngOnDestroy() { + this.clearMostRecentId(); + } + + private clearMostRecentId(): void { + if (this.mostRecentId !== undefined) { + this.contextHelpService.remove(this.mostRecentId); + } } } diff --git a/src/app/shared/context-help.model.ts b/src/app/shared/context-help.model.ts index 4b2b344f60..9c024dbf1f 100644 --- a/src/app/shared/context-help.model.ts +++ b/src/app/shared/context-help.model.ts @@ -1,4 +1,4 @@ -export class ContextHelpModel { +export class ContextHelp { id: string; - tooltipVisible?: boolean = false; + isTooltipVisible?: boolean = false; } diff --git a/src/app/shared/context-help.reducer.ts b/src/app/shared/context-help.reducer.ts index b5f683956f..b705c797ac 100644 --- a/src/app/shared/context-help.reducer.ts +++ b/src/app/shared/context-help.reducer.ts @@ -1,8 +1,8 @@ -import { ContextHelpModel } from './context-help.model'; +import { ContextHelp } from './context-help.model'; import { ContextHelpAction, ContextHelpActionTypes } from './context-help.actions'; export type ContextHelpModels = { - [id: string]: ContextHelpModel; + [id: string]: ContextHelp; }; export interface ContextHelpState { @@ -10,7 +10,10 @@ export interface ContextHelpState { models: ContextHelpModels; } -export function contextHelpReducer(state: ContextHelpState, action: ContextHelpAction): ContextHelpState { +// TODO +const initialState: ContextHelpState = {allIconsVisible: true, models: {}}; + +export function contextHelpReducer(state: ContextHelpState = initialState, action: ContextHelpAction): ContextHelpState { switch (action.type) { case ContextHelpActionTypes.CONTEXT_HELP_TOGGLE_ICONS: { return {...state, allIconsVisible: true}; @@ -41,7 +44,7 @@ export function contextHelpReducer(state: ContextHelpState, action: ContextHelpA function modifyTooltipVisibility(state: ContextHelpState, id: string, modify: (vis: boolean) => boolean) : ContextHelpState { const {[id]: matchingModel, ...otherModels} = state.models; - const modifiedModel = {...matchingModel, tooltipVisible: modify(matchingModel.tooltipVisible)}; + const modifiedModel = {...matchingModel, isTooltipVisible: modify(matchingModel.isTooltipVisible)}; const newModels = {...otherModels, [id]: modifiedModel}; return {...state, models: newModels}; } diff --git a/src/app/shared/context-help.service.ts b/src/app/shared/context-help.service.ts index 071308c713..7bbd1216e1 100644 --- a/src/app/shared/context-help.service.ts +++ b/src/app/shared/context-help.service.ts @@ -1,33 +1,100 @@ import { Injectable } from '@angular/core'; -import { ContextHelpModel } from './context-help.model'; +import { ContextHelp } from './context-help.model'; +import { Store, createFeatureSelector, createSelector, select, MemoizedSelector } from '@ngrx/store'; +import { ContextHelpState } from './context-help.reducer'; +import { + ContextHelpToggleIconsAction, + ContextHelpAddAction, + ContextHelpRemoveAction, + ContextHelpShowTooltipAction, + ContextHelpHideTooltipAction, + ContextHelpToggleTooltipAction +} from './context-help.actions'; +import { Observable } from 'rxjs'; + +const contextHelpStateSelector = + createFeatureSelector('contextHelp'); +const allIconsVisibleSelector = createSelector( + contextHelpStateSelector, + (state: ContextHelpState): boolean => state.allIconsVisible +); +const contextHelpSelector = + (id: string): MemoizedSelector => createSelector( + contextHelpStateSelector, + (state: ContextHelpState) => state.models[id] + ); @Injectable({ providedIn: 'root' }) export class ContextHelpService { - constructor() { } + constructor(private store: Store) { } + /** + * Observable keeping track of whether context help icons should be visible globally. + */ + shouldShowIcons$(): Observable { + return this.store.pipe(select(allIconsVisibleSelector)); + } + + /** + * Observable that tracks the state for a specific context help icon. + * + * @param id: id of the context help icon. + */ + getContextHelp$(id: string): Observable { + return this.store.pipe(select(contextHelpSelector(id))); + } + + /** + * Toggles the visibility of all context help icons. + */ toggleIcons() { - + this.store.dispatch(new ContextHelpToggleIconsAction()); } - add(contextHelp: ContextHelpModel) { - + /** + * Registers a new context help icon to the store. + * + * @param contextHelp: the initial state of the new help icon. + */ + add(contextHelp: ContextHelp) { + this.store.dispatch(new ContextHelpAddAction(contextHelp)); } + /** + * Removes a context help icon from the store. + * + * @id: the id of the help icon to be removed. + */ remove(id: string) { - - } - - showTooltip(id: string) { - - } - - hideTooltip(id: string) { - + this.store.dispatch(new ContextHelpRemoveAction(id)); } + /** + * Toggles the tooltip of a single context help icon. + * + * @id: the id of the help icon for which the visibility will be toggled. + */ toggleTooltip(id: string) { + this.store.dispatch(new ContextHelpToggleTooltipAction(id)); + } + /** + * Shows the tooltip of a single context help icon. + * + * @id: the id of the help icon that will be made visible. + */ + showTooltip(id: string) { + this.store.dispatch(new ContextHelpShowTooltipAction(id)); + } + + /** + * Hides the tooltip of a single context help icon. + * + * @id: the id of the help icon that will be made invisible. + */ + hideTooltip(id: string) { + this.store.dispatch(new ContextHelpHideTooltipAction(id)); } }