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 { 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<AppState> = {
@@ -92,7 +94,8 @@ export const appReducers: ActionReducerMap<AppState> = {
communityList: CommunityListReducer,
epeopleRegistry: ePeopleRegistryReducer,
groupRegistry: groupRegistryReducer,
correlationId: correlationIdReducer
correlationId: correlationIdReducer,
contextHelp: contextHelpReducer,
};
export const routerStateSelector = (state: AppState) => state.router;

View File

@@ -1,6 +1,6 @@
<div class="page-internal-server-error container">
<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/>
<p>{{"500.help" | translate}}</p>
<br/>

View File

@@ -10,12 +10,16 @@
</ng-container>
</div>
</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-left': iconPlacement === 'left'}"
[ngbTooltip]="help"
[placement]="tooltipPlacement"
autoClose="outside"
triggers="manual"
container="'body'"
triggers="click:blur">
#tooltip="ngbTooltip"
(click)="onClick()">
</i>
<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 { 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<any>;
/*
/**
* 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<boolean>;
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).

View File

@@ -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;
}
}

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 { 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<ContextHelpWrapperComponent>;
@@ -32,19 +46,31 @@ export class ContextHelpDirective implements OnChanges {
private templateRef: TemplateRef<any>,
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);
}
}
}

View File

@@ -1,4 +1,4 @@
export class ContextHelpModel {
export class ContextHelp {
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';
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};
}

View File

@@ -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<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({
providedIn: 'root'
})
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() {
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));
}
}