From 6d533f6858761ef35ff18f03d6a1916b22c74d50 Mon Sep 17 00:00:00 2001 From: Angelika Kinas Date: Thu, 21 Dec 2023 13:37:33 +0100 Subject: [PATCH 1/2] feat(ui): Adjust linkify directive to be able to process HTML elements and Text. Will be used additionally in the abstract and landing page. --- .../metadata-info/linkify.directive.spec.ts | 58 ++++++++++++++-- .../lib/metadata-info/linkify.directive.ts | 68 +++++++++++++++---- .../metadata-info.component.html | 7 +- 3 files changed, 111 insertions(+), 22 deletions(-) diff --git a/libs/ui/elements/src/lib/metadata-info/linkify.directive.spec.ts b/libs/ui/elements/src/lib/metadata-info/linkify.directive.spec.ts index 1fffa88aa2..541c624526 100644 --- a/libs/ui/elements/src/lib/metadata-info/linkify.directive.spec.ts +++ b/libs/ui/elements/src/lib/metadata-info/linkify.directive.spec.ts @@ -52,11 +52,35 @@ const testingUrls = [ 'http://foo.com/(something)?after=parens', ], ] + +const testWithMultipleUrls = { + input: + 'Fourteenth links http://foo.com/(something)?after=parens with multiple links http://foo.com/(something)?before=multiple', + output: [ + 'http://foo.com/(something)?after=parens', + 'http://foo.com/(something)?before=multiple', + ], +} + +const testWithHTML = { + input: + '

Fourteenth link with html input This is the display text query params

', + output: 'http://foo.com/(something)?after=before', +} + @Component({ - template: `
{{ text }}
`, + template: `
+
+ {{ text }} +
`, }) class TestComponent { text = '' + customInnerHTML = null } describe('GnUiLinkifyDirective', () => { @@ -80,23 +104,49 @@ describe('GnUiLinkifyDirective', () => { component.text = input fixture.detectChanges() await fixture.whenStable() - const href = getAnchorElement().nativeElement.getAttribute('href') + const href = getAnchorElement()[0].nativeElement.getAttribute('href') expect(href).toBe(output) } ) + + it('should create multiple anchor elements with the correct href', async () => { + component.text = testWithMultipleUrls.input + const output = testWithMultipleUrls.output + fixture.detectChanges() + await fixture.whenStable() + const amountOfAnchors = getAnchorElement().length + const firstHref = getAnchorElement()[0].nativeElement.getAttribute('href') + const secondHref = + getAnchorElement()[1].nativeElement.getAttribute('href') + expect(amountOfAnchors).toBe(2) + expect(firstHref).toBe(output[0]) + expect(secondHref).toBe(output[1]) + }) }) it('should have the target attribute set to "_blank"', async () => { component.text = 'Click this link https://www.example.com/' fixture.detectChanges() await fixture.whenStable() - const target = getAnchorElement().nativeElement.getAttribute('target') + const target = getAnchorElement()[0].nativeElement.getAttribute('target') expect(target).toBe('_blank') }) function getAnchorElement() { debugElement = fixture.debugElement.query( By.directive(GnUiLinkifyDirective) ) - return debugElement.query(By.css('a')) + return debugElement.queryAll(By.css('a')) } + + describe('HTML input', () => { + it('should create an anchor element with the correct href', async () => { + component.customInnerHTML = testWithHTML.input + fixture.detectChanges() + await fixture.whenStable() + const href = getAnchorElement()[0].nativeElement.getAttribute('href') + const matIcon = getAnchorElement()[0].nativeElement.childNodes[1] + expect(href).toBe(testWithHTML.output) + expect(matIcon.nodeName).toContain('MAT-ICON') + }) + }) }) diff --git a/libs/ui/elements/src/lib/metadata-info/linkify.directive.ts b/libs/ui/elements/src/lib/metadata-info/linkify.directive.ts index 6f226ce041..24cbb95636 100644 --- a/libs/ui/elements/src/lib/metadata-info/linkify.directive.ts +++ b/libs/ui/elements/src/lib/metadata-info/linkify.directive.ts @@ -9,30 +9,72 @@ export class GnUiLinkifyDirective implements OnInit { ngOnInit() { setTimeout(() => { - this.processLinks() + this.processLinks(this.el.nativeElement) }, 0) } - private processLinks() { - const container = this.el.nativeElement - + private processLinks(container: HTMLElement | ChildNode) { const nodes = Array.from(container.childNodes) + nodes.forEach((node) => { if (node instanceof Text) { const textNode = node as Text - const linkified = this.linkifyText(textNode.nodeValue) - const span = this.renderer.createElement('span') - span.innerHTML = linkified - container.insertBefore(span, textNode) - container.removeChild(textNode) + const linkified = this.linkifyNode(textNode.nodeValue) + if (linkified) { + this.createLinkElements(container, linkified, node) + } + } else if (node instanceof HTMLAnchorElement) { + const url = node.href + const displayValue = node.innerHTML + const linkified = this.linkifyNode(displayValue, url) + if (linkified) { + this.createLinkElements(container, linkified, node) + } + } else { + this.processLinks(node) } }) } - private linkifyText(text: string): string { - return text.replace(/(\bhttps?:\/\/\S+\b[=)/]?)/g, (match) => { - return `${match} open_in_new` + private linkifyNode(displayValue: string, url?: string): string | undefined { + if (url) { + displayValue = this.createLink(displayValue, url) + } else { + const urlRegex = /\bhttps?:\/\/\S+\b[=)/]?/g + const matches = displayValue.match(urlRegex) + + if (matches && matches.length > 0) { + matches.forEach((match) => { + url = match + + displayValue = displayValue.replace(match, (match) => { + return this.createLink(match, url) + }) + }) + } + } + + return displayValue + } + + private createLinkElements( + container: HTMLElement | ChildNode, + htmlContent: string, + node: ChildNode + ): void { + const div = this.renderer.createElement('div') + div.innerHTML = htmlContent + + const fragment = document.createDocumentFragment() + Array.from(div.childNodes).forEach((childNode: ChildNode) => { + fragment.appendChild(childNode) }) + + container.insertBefore(fragment, node) + container.removeChild(node) + } + + private createLink(displayValue: string, url: string): string { + return `${displayValue} open_in_new` } } diff --git a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html index d614a93be2..369eff7235 100644 --- a/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html +++ b/libs/ui/elements/src/lib/metadata-info/metadata-info.component.html @@ -11,6 +11,7 @@

@@ -100,11 +101,7 @@

record.metadata.sheet

- open_in_new - {{ metadata.landingPage }} + {{ metadata.landingPage }}

From e4bb3c2f0ed301932e9a6d3e7d79365c8ec717ad Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 1 Jan 2024 14:45:14 +0100 Subject: [PATCH 2/2] feat(ui): improve sample HTML for linkify storybook --- .../linkify.directive.stories.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/libs/ui/elements/src/lib/metadata-info/linkify.directive.stories.ts b/libs/ui/elements/src/lib/metadata-info/linkify.directive.stories.ts index 3bca5ce296..2d55fd76ce 100644 --- a/libs/ui/elements/src/lib/metadata-info/linkify.directive.stories.ts +++ b/libs/ui/elements/src/lib/metadata-info/linkify.directive.stories.ts @@ -1,9 +1,4 @@ -import { - componentWrapperDecorator, - Meta, - moduleMetadata, - StoryObj, -} from '@storybook/angular' +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' import { GnUiLinkifyDirective } from './linkify.directive' export default { @@ -17,13 +12,19 @@ export default { export const Primary: StoryObj = { args: { - htmlContent: `Région Hauts-de-France, Dreal, IGN BD Topo
- - Les données produites s'appuient sur le modèle CNIG de juin 2018 relatif aux SCoT : http://cnig.gouv.fr/wp-content/uploads/2019/04/190315_Standard_CNIG_SCOT.pdf
- - La structure a été modifiée au 03/2023 pour prendre en compte les évolutions du modèle CNIG du 10/06/2021 :
- http://cnig.gouv.fr/IMG/pdf/210615_standard_cnig_nouveauscot.pdf
- (il coexiste donc dans le modèle des champs liés aux deux modèles, par exemple sur les PADD pour les "anciens" SCoT, ou encore sur les PAS ou les DAAC pour les "nouveaux" SCoT)`, + htmlContent: `

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin purus elit, tincidunt et gravida sit amet, mattis eget orci. Suspendisse dignissim magna sed neque rutrum lobortis. Aenean vitae quam sapien. Phasellus eleifend tortor ac imperdiet tristique. Curabitur aliquet mauris tristique, iaculis est sit amet, pulvinar ipsum. Maecenas lacinia varius felis sit amet tempor. Curabitur pulvinar ipsum eros, quis accumsan odio hendrerit sit amet.

+This is a link without markup: http://cnig.gouv.fr/wp-content/uploads/2019/04/190315_Standard_CNIG_SCOT.pdf
+Another link without markup:
+http://cnig.gouv.fr/IMG/pdf/210615_standard_cnig_nouveauscot.pdf
+

This is a link with markup: This is the display text

+This is a list containing links: +`, }, argTypes: { htmlContent: { @@ -33,8 +34,7 @@ export const Primary: StoryObj = { render: (args) => ({ props: args, template: ` -
+
${args.htmlContent}
`, }),