diff --git a/src/app/shared/context-help-wrapper/context-help-wrapper.component.spec.ts b/src/app/shared/context-help-wrapper/context-help-wrapper.component.spec.ts index 3ac27f7090..c0c7f9a821 100644 --- a/src/app/shared/context-help-wrapper/context-help-wrapper.component.spec.ts +++ b/src/app/shared/context-help-wrapper/context-help-wrapper.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { of as observableOf, Observable, BehaviorSubject } from 'rxjs'; +import { of as observableOf, BehaviorSubject } from 'rxjs'; import { ContextHelpWrapperComponent } from './context-help-wrapper.component'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; @@ -20,6 +20,7 @@ import { By } from '@angular/platform-browser'; [id]="id" [tooltipPlacement]="tooltipPlacement" [iconPlacement]="iconPlacement" + [dontParseLinks]="dontParseLinks" > ` @@ -29,11 +30,12 @@ class TemplateComponent { @Input() id: string; @Input() tooltipPlacement?: PlacementArray; @Input() iconPlacement?: PlacementDir; + @Input() dontParseLinks?: boolean; } const messages = { lorem: 'lorem ipsum dolor sit amet', - linkTest: 'This is text, [this](https://dspace.lyrasis.org) is a link, and [so is this](https://google.com)' + linkTest: 'This is text, [this](https://dspace.lyrasis.org/) is a link, and [so is this](https://google.com/)' }; const exampleContextHelp: ContextHelp = { id: 'test-tooltip', @@ -147,6 +149,55 @@ describe('ContextHelpWrapperComponent', () => { fixture.detectChanges(); expect(wrapperComponent.tooltip.open).toHaveBeenCalled(); expect(wrapperComponent.tooltip.close).toHaveBeenCalledTimes(0); + expect(fixture.debugElement.query(By.css('.ds-context-help-content')).nativeElement.textContent) + .toMatch(/\s*lorem ipsum dolor sit amet\s*/); + }); + + it('should correctly display links', () => { + templateComponent.content = 'linkTest'; + getContextHelp$.next({...exampleContextHelp, isTooltipVisible: true}); + fixture.detectChanges(); + const nodeList: NodeList = fixture.debugElement.query(By.css('.ds-context-help-content')) + .nativeElement + .childNodes; + const relevantNodes = Array.from(nodeList).filter(node => node.nodeType != Node.COMMENT_NODE); + expect(relevantNodes.length).toBe(4); + + const [text1, link1, text2, link2] = relevantNodes; + + expect(text1.nodeType).toBe(Node.TEXT_NODE); + expect(text1.nodeValue).toMatch(/\s* This is text, \s*/); + + expect(link1.nodeName).toBe('A'); + expect((link1 as any).href).toBe('https://dspace.lyrasis.org/'); + expect(link1.textContent).toBe('this'); + + expect(text2.nodeType).toBe(Node.TEXT_NODE); + expect(text2.nodeValue).toMatch(/\s* is a link, and \s*/); + + expect(link2.nodeName).toBe('A'); + expect((link2 as any).href).toBe('https://google.com/'); + expect(link2.textContent).toBe('so is this'); + }); + + it('should not display links if specified not to', () => { + templateComponent.dontParseLinks = true; + templateComponent.content = 'linkTest'; + getContextHelp$.next({...exampleContextHelp, isTooltipVisible: true}); + fixture.detectChanges(); + + + const nodeList: NodeList = fixture.debugElement.query(By.css('.ds-context-help-content')) + .nativeElement + .childNodes; + const relevantNodes = Array.from(nodeList).filter(node => node.nodeType != Node.COMMENT_NODE); + expect(relevantNodes.length).toBe(1); + + const [text] = relevantNodes; + + expect(text.nodeType).toBe(Node.TEXT_NODE); + expect(text.nodeValue).toMatch( + /\s* This is text, \[this\]\(https:\/\/dspace.lyrasis.org\/\) is a link, and \[so is this\]\(https:\/\/google.com\/\) \s*/); }); describe('after the icon is clicked again', () => { @@ -165,6 +216,4 @@ describe('ContextHelpWrapperComponent', () => { }); }); }); - - // TODO: link parsing tests }); 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 260c2b05dd..1bf3dc1ce0 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,8 +1,8 @@ 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, Subscription } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable, of as observableOf, Subscription, BehaviorSubject, combineLatest } from 'rxjs'; +import { map, distinctUntilChanged, mergeMap } from 'rxjs/operators'; import { PlacementDir } from './placement-dir.model'; import content from '*.scss'; import { ContextHelpService } from '../context-help.service'; @@ -10,6 +10,8 @@ import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { hasValueOperator } from '../empty.util'; import { ContextHelp } from '../context-help.model'; +type ParsedContent = (string | {href: string, text: string})[]; + /** * This component renders an info icon next to the wrapped element which * produces a tooltip when clicked. @@ -44,21 +46,21 @@ export class ContextHelpWrapperComponent implements OnInit, OnDestroy { /** * If true, don't process text to render links. */ - @Input() dontParseLinks?: boolean; + @Input() set dontParseLinks(dont: boolean) { + this.dontParseLinks$.next(dont); + } + private dontParseLinks$: BehaviorSubject = new BehaviorSubject(false); shouldShowIcon$: Observable; tooltip: NgbTooltip; - // TODO: dependent on evaluation order of input setters? - parsedContent$: Observable<(string | {href: string, text: string})[]> = observableOf([]); @Input() set content(content : string) { - this.parsedContent$ = this.translateService.get(content).pipe( - map(this.dontParseLinks - ? ((text: string) => [text]) - : this.parseLinks) - ); + this.content$.next(content); } + private content$: BehaviorSubject = new BehaviorSubject(undefined); + + parsedContent$: Observable; private subs: {always: Subscription[], tooltipBound: Subscription[]} = {always: [], tooltipBound: []}; @@ -69,8 +71,15 @@ export class ContextHelpWrapperComponent implements OnInit, OnDestroy { ) { } ngOnInit() { + this.parsedContent$ = combineLatest([ + this.content$.pipe(distinctUntilChanged(), mergeMap(content => this.translateService.get(content))), + this.dontParseLinks$.pipe(distinctUntilChanged()) + ]).pipe( + map(([content, dontParseLinks]) => + dontParseLinks ? [content] : this.parseLinks(content)) + ); this.shouldShowIcon$ = this.contextHelpService.shouldShowIcons$(); - this.subs.always = [this.shouldShowIcon$.subscribe()]; + this.subs.always = [this.parsedContent$.subscribe(), this.shouldShowIcon$.subscribe()]; } @ViewChild('tooltip', { static: false }) set setTooltip(tooltip: NgbTooltip) { @@ -127,8 +136,8 @@ export class ContextHelpWrapperComponent implements OnInit, OnDestroy { * {href: "https://youtube.com", text: "so is this"} * ] */ - private parseLinks(content: string): (string | {href: string, text: string})[] { - // Implementation note: due to unavailability of `matchAll` method on strings, + private parseLinks(content: string): ParsedContent { + // Implementation note: due to `matchAll` method on strings not being available for all versions, // separate "split" and "parse" steps are needed. // We use splitRegexp (the outer `match` call) to split the text