97732 Context help service, changes to directive and component

This commit is contained in:
Koen Pauwels
2023-01-04 15:54:09 +01:00
parent c156e1ca6f
commit 5ba45cb0fa
9 changed files with 206 additions and 48 deletions

View File

@@ -50,6 +50,7 @@ import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer'; import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
import { correlationIdReducer } from './correlation-id/correlation-id.reducer'; import { correlationIdReducer } from './correlation-id/correlation-id.reducer';
import { contextHelpReducer, ContextHelpState } from './shared/context-help.reducer';
export interface AppState { export interface AppState {
router: fromRouter.RouterReducerState; router: fromRouter.RouterReducerState;
@@ -71,6 +72,7 @@ export interface AppState {
epeopleRegistry: EPeopleRegistryState; epeopleRegistry: EPeopleRegistryState;
groupRegistry: GroupRegistryState; groupRegistry: GroupRegistryState;
correlationId: string; correlationId: string;
contextHelp: ContextHelpState;
} }
export const appReducers: ActionReducerMap<AppState> = { export const appReducers: ActionReducerMap<AppState> = {
@@ -92,7 +94,8 @@ export const appReducers: ActionReducerMap<AppState> = {
communityList: CommunityListReducer, communityList: CommunityListReducer,
epeopleRegistry: ePeopleRegistryReducer, epeopleRegistry: ePeopleRegistryReducer,
groupRegistry: groupRegistryReducer, groupRegistry: groupRegistryReducer,
correlationId: correlationIdReducer correlationId: correlationIdReducer,
contextHelp: contextHelpReducer,
}; };
export const routerStateSelector = (state: AppState) => state.router; export const routerStateSelector = (state: AppState) => state.router;

View File

@@ -1,6 +1,6 @@
<div class="page-internal-server-error container"> <div class="page-internal-server-error container">
<h1>500</h1> <h1>500</h1>
<h2><small *dsContextHelp="{content: 'context-help.multi-para.test'}">{{"500.page-internal-server-error" | translate}}</small></h2> <h2><small *dsContextHelp="{content: 'context-help.multi-para.test', id: 'server-error'}">{{"500.page-internal-server-error" | translate}}</small></h2>
<br/> <br/>
<p>{{"500.help" | translate}}</p> <p>{{"500.help" | translate}}</p>
<br/> <br/>

View File

@@ -10,12 +10,16 @@
</ng-container> </ng-container>
</div> </div>
</ng-template> </ng-template>
<i [ngClass]="{'ds-context-help-icon fas fa-question-circle shadow-sm': true, <i *ngIf="shouldShowIcon$ | async"
[ngClass]="{'ds-context-help-icon fas fa-question-circle shadow-sm': true,
'ds-context-help-icon-right': iconPlacement !== 'left', 'ds-context-help-icon-right': iconPlacement !== 'left',
'ds-context-help-icon-left': iconPlacement === 'left'}" 'ds-context-help-icon-left': iconPlacement === 'left'}"
[ngbTooltip]="help" [ngbTooltip]="help"
[placement]="tooltipPlacement" [placement]="tooltipPlacement"
autoClose="outside"
triggers="manual"
container="'body'" container="'body'"
triggers="click:blur"> #tooltip="ngbTooltip"
(click)="onClick()">
</i> </i>
<ng-container *ngTemplateOutlet="templateRef"></ng-container> <ng-container *ngTemplateOutlet="templateRef"></ng-container>

View File

@@ -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 { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning';
import { TranslateService } from '@ngx-translate/core'; 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 { map } from 'rxjs/operators';
import { PlacementDir } from './placement-dir.model'; 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 * This component renders an info icon next to the wrapped element which
* produces a tooltip when clicked. * produces a tooltip when clicked.
*/ */
@@ -14,28 +19,39 @@ import { PlacementDir } from './placement-dir.model';
templateUrl: './context-help-wrapper.component.html', templateUrl: './context-help-wrapper.component.html',
styleUrls: ['./context-help-wrapper.component.scss'], styleUrls: ['./context-help-wrapper.component.scss'],
}) })
export class ContextHelpWrapperComponent { export class ContextHelpWrapperComponent implements OnInit, AfterViewInit, OnDestroy {
/* /**
* Template reference for the wrapped element. * Template reference for the wrapped element.
*/ */
@Input() templateRef: TemplateRef<any>; @Input() templateRef: TemplateRef<any>;
/* /**
* Identifier for the context help tooltip.
*/
@Input() id: string;
/**
* Indicate where the tooltip should show up, relative to the info icon. * Indicate where the tooltip should show up, relative to the info icon.
*/ */
@Input() tooltipPlacement?: PlacementArray; @Input() tooltipPlacement?: PlacementArray;
/* /**
* Indicate whether the info icon should appear to the left or to * Indicate whether the info icon should appear to the left or to
* the right of the wrapped element. * the right of the wrapped element.
*/ */
@Input() iconPlacement?: PlacementDir; @Input() iconPlacement?: PlacementDir;
/* /**
* If true, don't process text to render links. * If true, don't process text to render links.
*/ */
@Input() dontParseLinks?: boolean; @Input() dontParseLinks?: boolean;
@ViewChild('tooltip', { static: false }) tooltip: NgbTooltip;
shouldShowIcon$: Observable<boolean>;
private subs: Subscription[] = [];
// TODO: dependent on evaluation order of input setters? // TODO: dependent on evaluation order of input setters?
parsedContent$: Observable<(string | {href: string, text: string})[]> = observableOf([]); parsedContent$: Observable<(string | {href: string, text: string})[]> = observableOf([]);
@Input() set content(content : string) { @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 * Parses Markdown-style links, splitting up a given text
* into link-free pieces of text and objects of the form * into link-free pieces of text and objects of the form
* {href: string, text: string} (which represent links). * {href: string, text: string} (which represent links).

View File

@@ -1,6 +1,6 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { type } from './ngrx/type'; import { type } from './ngrx/type';
import { ContextHelpModel } from './context-help.model'; import { ContextHelp } from './context-help.model';
export const ContextHelpActionTypes = { export const ContextHelpActionTypes = {
'CONTEXT_HELP_TOGGLE_ICONS': type('dspace/context-help/CONTEXT_HELP_TOGGLE_ICONS'), '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 { export class ContextHelpAddAction implements Action {
type = ContextHelpActionTypes.CONTEXT_HELP_ADD; type = ContextHelpActionTypes.CONTEXT_HELP_ADD;
model: ContextHelpModel; model: ContextHelp;
constructor (model: ContextHelpModel) { constructor (model: ContextHelp) {
this.model = model; this.model = model;
} }
} }

View File

@@ -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 { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning';
import { ContextHelpWrapperComponent } from './context-help-wrapper/context-help-wrapper.component'; import { ContextHelpWrapperComponent } from './context-help-wrapper/context-help-wrapper.component';
import { PlacementDir } from './context-help-wrapper/placement-dir.model'; import { PlacementDir } from './context-help-wrapper/placement-dir.model';
import { ContextHelpService } from './context-help.service';
export interface ContextHelpDirectiveInput { export interface ContextHelpDirectiveInput {
content: string; content: string;
id: string;
tooltipPlacement?: PlacementArray; tooltipPlacement?: PlacementArray;
iconPlacement?: PlacementDir; iconPlacement?: PlacementDir;
} }
/* /**
* Directive to add a clickable tooltip icon to an element. * Directive to add a clickable tooltip icon to an element.
* The tooltip icon's position is configurable ('left' or 'right') * The tooltip icon's position is configurable ('left' or 'right')
* and so is the position of the tooltip itself (PlacementArray). * and so is the position of the tooltip itself (PlacementArray).
@@ -17,14 +30,15 @@ export interface ContextHelpDirectiveInput {
@Directive({ @Directive({
selector: '[dsContextHelp]', selector: '[dsContextHelp]',
}) })
export class ContextHelpDirective implements OnChanges { export class ContextHelpDirective implements OnChanges, OnDestroy {
/* /**
* Expects an object with the following fields: * Expects an object with the following fields:
* - content: a string referring to an entry in the i18n files * - 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 * - 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 * - 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<ContextHelpWrapperComponent>; protected wrapper: ComponentRef<ContextHelpWrapperComponent>;
@@ -32,19 +46,31 @@ export class ContextHelpDirective implements OnChanges {
private templateRef: TemplateRef<any>, private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private contextHelpService: ContextHelpService
){} ){}
ngOnChanges() { ngOnChanges() {
const input: ContextHelpDirectiveInput = typeof(this.dsContextHelp) === 'string' this.clearMostRecentId();
? {content: this.dsContextHelp} this.mostRecentId = this.dsContextHelp.id;
: this.dsContextHelp; this.contextHelpService.add({id: this.dsContextHelp.id}); // TODO: tooltipVisible field
const factory const factory
= this.componentFactoryResolver.resolveComponentFactory(ContextHelpWrapperComponent); = this.componentFactoryResolver.resolveComponentFactory(ContextHelpWrapperComponent);
this.wrapper = this.viewContainerRef.createComponent(factory); this.wrapper = this.viewContainerRef.createComponent(factory);
this.wrapper.instance.templateRef = this.templateRef; this.wrapper.instance.templateRef = this.templateRef;
this.wrapper.instance.content = input.content; this.wrapper.instance.content = this.dsContextHelp.content;
this.wrapper.instance.tooltipPlacement = input.tooltipPlacement; this.wrapper.instance.id = this.dsContextHelp.id;
this.wrapper.instance.iconPlacement = input.iconPlacement; 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);
}
} }
} }

View File

@@ -1,4 +1,4 @@
export class ContextHelpModel { export class ContextHelp {
id: string; id: string;
tooltipVisible?: boolean = false; isTooltipVisible?: boolean = false;
} }

View File

@@ -1,8 +1,8 @@
import { ContextHelpModel } from './context-help.model'; import { ContextHelp } from './context-help.model';
import { ContextHelpAction, ContextHelpActionTypes } from './context-help.actions'; import { ContextHelpAction, ContextHelpActionTypes } from './context-help.actions';
export type ContextHelpModels = { export type ContextHelpModels = {
[id: string]: ContextHelpModel; [id: string]: ContextHelp;
}; };
export interface ContextHelpState { export interface ContextHelpState {
@@ -10,7 +10,10 @@ export interface ContextHelpState {
models: ContextHelpModels; 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) { switch (action.type) {
case ContextHelpActionTypes.CONTEXT_HELP_TOGGLE_ICONS: { case ContextHelpActionTypes.CONTEXT_HELP_TOGGLE_ICONS: {
return {...state, allIconsVisible: true}; 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) function modifyTooltipVisibility(state: ContextHelpState, id: string, modify: (vis: boolean) => boolean)
: ContextHelpState { : ContextHelpState {
const {[id]: matchingModel, ...otherModels} = state.models; 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}; const newModels = {...otherModels, [id]: modifiedModel};
return {...state, models: newModels}; return {...state, models: newModels};
} }

View File

@@ -1,33 +1,100 @@
import { Injectable } from '@angular/core'; 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<ContextHelpState>('contextHelp');
const allIconsVisibleSelector = createSelector(
contextHelpStateSelector,
(state: ContextHelpState): boolean => state.allIconsVisible
);
const contextHelpSelector =
(id: string): MemoizedSelector<ContextHelpState, ContextHelp> => createSelector(
contextHelpStateSelector,
(state: ContextHelpState) => state.models[id]
);
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ContextHelpService { export class ContextHelpService {
constructor() { } constructor(private store: Store<ContextHelpState>) { }
/**
* Observable keeping track of whether context help icons should be visible globally.
*/
shouldShowIcons$(): Observable<boolean> {
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<ContextHelp> {
return this.store.pipe(select(contextHelpSelector(id)));
}
/**
* Toggles the visibility of all context help icons.
*/
toggleIcons() { 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) { remove(id: string) {
this.store.dispatch(new ContextHelpRemoveAction(id));
}
showTooltip(id: string) {
}
hideTooltip(id: string) {
} }
/**
* 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) { 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));
} }
} }