From 379da42f2e5795dcd5eb63b7264678b8129d5fdf Mon Sep 17 00:00:00 2001 From: Anastasia <155824290+kryzanivska-nastya@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:58:57 +0200 Subject: [PATCH 1/6] add link redirection in comment textarea --- .../comment-textarea.component.html | 1 + .../comment-textarea.component.ts | 36 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.html b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.html index 178dbd06f9..8d3c65f03c 100644 --- a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.html +++ b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.html @@ -11,6 +11,7 @@ (paste)="onPaste($event)" tabindex="0" [ngClass]="{ invalid: content.errors?.maxlength }" + (blur)="onCommentTextareaBlur()" >

{{ 'homepage.eco-news.comment.reply-error-message' | translate }} diff --git a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts index eb70b4868e..ab531c9e58 100644 --- a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts +++ b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts @@ -21,6 +21,7 @@ import { debounceTime, filter, takeUntil, tap } from 'rxjs/operators'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { CHAT_ICONS } from 'src/app/chat/chat-icons'; import { insertEmoji } from '../add-emoji/add-emoji'; +import { Patterns } from '@assets/patterns/patterns'; @Component({ selector: 'app-comment-textarea', @@ -115,7 +116,17 @@ export class CommentTextareaComponent implements OnInit, AfterViewInit, OnChange } private handleInputChange(): void { - this.content.setValue(this.commentTextarea.nativeElement.textContent); + const textContent = this.commentTextarea.nativeElement.textContent; + + if (this.content.value !== textContent) { + this.content.setValue(textContent); + } + + if (Patterns.linkPattern.test(textContent)) { + this.commentTextarea.nativeElement.innerHTML = this.renderLinks(textContent); + this.initializeLinkClickListeners(this.commentTextarea.nativeElement); + } + this.emitComment(); this.closeDropdownIfNoTag(); } @@ -211,6 +222,13 @@ export class CommentTextareaComponent implements OnInit, AfterViewInit, OnChange selection.addRange(range); } + onCommentTextareaBlur(): void { + const strippedText = this.commentTextarea.nativeElement.textContent; + this.commentTextarea.nativeElement.textContent = strippedText; + this.content.setValue(strippedText); + this.emitComment(); + } + onCommentKeyDown(event: KeyboardEvent): void { if (event.key === 'Enter' || event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); @@ -267,6 +285,22 @@ export class CommentTextareaComponent implements OnInit, AfterViewInit, OnChange }); } + renderLinks(text: string): string { + return text.replace(Patterns.urlLinkifyPattern, (match) => { + return `${match}`; + }); + } + + initializeLinkClickListeners(element: HTMLElement): void { + const links = element.querySelectorAll('a'); + links.forEach((link: HTMLAnchorElement) => { + link.addEventListener('click', (event) => { + event.preventDefault(); + window.open(link.href, '_blank', 'noopener,noreferrer'); + }); + }); + } + private insertTextAtCursor(text: string): void { const selection = window.getSelection(); const range = selection.getRangeAt(0); From 26f0c86f2e18f19f0d12f07a0e7dae0cfa425c4c Mon Sep 17 00:00:00 2001 From: Anastasia <155824290+kryzanivska-nastya@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:19:57 +0200 Subject: [PATCH 2/6] small fix --- .../comment-textarea/comment-textarea.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts index ab531c9e58..8174f42598 100644 --- a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts +++ b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts @@ -208,8 +208,9 @@ export class CommentTextareaComponent implements OnInit, AfterViewInit, OnChange onCommentTextareaFocus(): void { const currentText = this.commentTextarea.nativeElement.textContent.trim(); - if (currentText === 'Add a comment' || currentText === '') { - this.commentTextarea.nativeElement.textContent = ''; + if (Patterns.urlLinkifyPattern.test(currentText)) { + this.commentTextarea.nativeElement.innerHTML = this.renderLinks(currentText); + this.initializeLinkClickListeners(this.commentTextarea.nativeElement); } const range = document.createRange(); From 2c147f390aa2e11afd1d9e1dd6e118dca2a086b4 Mon Sep 17 00:00:00 2001 From: Anastasia <155824290+kryzanivska-nastya@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:35:08 +0200 Subject: [PATCH 3/6] small fix --- .../components/comment-textarea/comment-textarea.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts index 8174f42598..64ce0bb4fe 100644 --- a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts +++ b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts @@ -208,6 +208,9 @@ export class CommentTextareaComponent implements OnInit, AfterViewInit, OnChange onCommentTextareaFocus(): void { const currentText = this.commentTextarea.nativeElement.textContent.trim(); + if (currentText === 'Add a comment' || currentText === '') { + this.commentTextarea.nativeElement.textContent = ''; + } if (Patterns.urlLinkifyPattern.test(currentText)) { this.commentTextarea.nativeElement.innerHTML = this.renderLinks(currentText); this.initializeLinkClickListeners(this.commentTextarea.nativeElement); From 4d693afc27361b1bb680bbc3bbd84ea65bd15773 Mon Sep 17 00:00:00 2001 From: Anastasia <155824290+kryzanivska-nastya@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:40:17 +0200 Subject: [PATCH 4/6] remove dublications --- .../comment-textarea.component.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts index 64ce0bb4fe..451aebcdc9 100644 --- a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts +++ b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts @@ -122,10 +122,7 @@ export class CommentTextareaComponent implements OnInit, AfterViewInit, OnChange this.content.setValue(textContent); } - if (Patterns.linkPattern.test(textContent)) { - this.commentTextarea.nativeElement.innerHTML = this.renderLinks(textContent); - this.initializeLinkClickListeners(this.commentTextarea.nativeElement); - } + this.updateLinksInTextarea(textContent); this.emitComment(); this.closeDropdownIfNoTag(); @@ -211,11 +208,8 @@ export class CommentTextareaComponent implements OnInit, AfterViewInit, OnChange if (currentText === 'Add a comment' || currentText === '') { this.commentTextarea.nativeElement.textContent = ''; } - if (Patterns.urlLinkifyPattern.test(currentText)) { - this.commentTextarea.nativeElement.innerHTML = this.renderLinks(currentText); - this.initializeLinkClickListeners(this.commentTextarea.nativeElement); - } + this.updateLinksInTextarea(currentText); const range = document.createRange(); const nodeAmount = this.commentTextarea.nativeElement.childNodes.length; range.setStartAfter(this.commentTextarea.nativeElement.childNodes[nodeAmount - 1]); @@ -289,6 +283,13 @@ export class CommentTextareaComponent implements OnInit, AfterViewInit, OnChange }); } + private updateLinksInTextarea(currentText: string): void { + if (Patterns.urlLinkifyPattern.test(currentText)) { + this.commentTextarea.nativeElement.innerHTML = this.renderLinks(currentText); + this.initializeLinkClickListeners(this.commentTextarea.nativeElement); + } + } + renderLinks(text: string): string { return text.replace(Patterns.urlLinkifyPattern, (match) => { return `${match}`; From 6f0c7bb365afc2cc2ddf1540f80482e3b7371ee6 Mon Sep 17 00:00:00 2001 From: Anastasia <155824290+kryzanivska-nastya@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:33:57 +0200 Subject: [PATCH 5/6] change according to comments --- .../comment-textarea.component.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts index 451aebcdc9..b033d7c047 100644 --- a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts +++ b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.ts @@ -222,7 +222,6 @@ export class CommentTextareaComponent implements OnInit, AfterViewInit, OnChange onCommentTextareaBlur(): void { const strippedText = this.commentTextarea.nativeElement.textContent; - this.commentTextarea.nativeElement.textContent = strippedText; this.content.setValue(strippedText); this.emitComment(); } @@ -285,24 +284,31 @@ export class CommentTextareaComponent implements OnInit, AfterViewInit, OnChange private updateLinksInTextarea(currentText: string): void { if (Patterns.urlLinkifyPattern.test(currentText)) { - this.commentTextarea.nativeElement.innerHTML = this.renderLinks(currentText); - this.initializeLinkClickListeners(this.commentTextarea.nativeElement); + const sanitizedHtml = this.sanitizer.sanitize(SecurityContext.HTML, this.renderLinks(currentText)); + if (sanitizedHtml) { + this.commentTextarea.nativeElement.innerHTML = sanitizedHtml; + this.initializeLinkClickListeners(this.commentTextarea.nativeElement); + } } } renderLinks(text: string): string { return text.replace(Patterns.urlLinkifyPattern, (match) => { - return `${match}`; + const safeUrl = this.sanitizer.sanitize(SecurityContext.URL, match) || ''; + return `${match}`; }); } initializeLinkClickListeners(element: HTMLElement): void { - const links = element.querySelectorAll('a'); - links.forEach((link: HTMLAnchorElement) => { - link.addEventListener('click', (event) => { + element.addEventListener('click', (event) => { + const target = event.target as HTMLElement; + if (target.tagName === 'A') { event.preventDefault(); - window.open(link.href, '_blank', 'noopener,noreferrer'); - }); + const href = target.getAttribute('href'); + if (href) { + window.open(href, '_blank', 'noopener,noreferrer'); + } + } }); } From 55d78ae9aaf6ea704e2ee94ba67c1894b3207ef3 Mon Sep 17 00:00:00 2001 From: Anastasia <155824290+kryzanivska-nastya@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:59:32 +0200 Subject: [PATCH 6/6] add tests --- .../comment-textarea.component.spec.ts | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.spec.ts b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.spec.ts index 15db77d09f..21c67212fc 100644 --- a/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.spec.ts +++ b/src/app/main/component/comments/components/comment-textarea/comment-textarea.component.spec.ts @@ -5,17 +5,19 @@ import { SocketService } from '@global-service/socket/socket.service'; import { LocalStorageService } from '@global-service/localstorage/local-storage.service'; import { BehaviorSubject, Observable } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; -import { By } from '@angular/platform-browser'; +import { By, DomSanitizer } from '@angular/platform-browser'; import { PlaceholderForDivDirective } from 'src/app/main/component/comments/directives/placeholder-for-div.directive'; import { MatSelectModule } from '@angular/material/select'; import { UserProfileImageComponent } from '@global-user/components/shared/components/user-profile-image/user-profile-image.component'; import { MatMenuModule } from '@angular/material/menu'; import { MatIconModule } from '@angular/material/icon'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ElementRef } from '@angular/core'; describe('CommentTextareaComponent', () => { let component: CommentTextareaComponent; let fixture: ComponentFixture; + let mockSanitizer: jasmine.SpyObj; const socketServiceMock: SocketService = jasmine.createSpyObj('SocketService', ['onMessage', 'send', 'initiateConnection']); socketServiceMock.onMessage = () => new Observable(); @@ -45,12 +47,14 @@ describe('CommentTextareaComponent', () => { ]; beforeEach(waitForAsync(() => { + mockSanitizer = jasmine.createSpyObj('DomSanitizer', ['sanitize']); TestBed.configureTestingModule({ declarations: [CommentTextareaComponent, PlaceholderForDivDirective, UserProfileImageComponent], providers: [ { provide: Router, useValue: {} }, { provide: SocketService, useValue: socketServiceMock }, - { provide: LocalStorageService, useValue: localStorageServiceMock } + { provide: LocalStorageService, useValue: localStorageServiceMock }, + { provide: DomSanitizer, useValue: mockSanitizer } ], imports: [MatSelectModule, TranslateModule.forRoot(), BrowserAnimationsModule, MatMenuModule, MatIconModule] }).compileComponents(); @@ -64,6 +68,7 @@ describe('CommentTextareaComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CommentTextareaComponent); component = fixture.componentInstance; + component.commentTextarea = new ElementRef(document.createElement('div')); fixture.detectChanges(); }); @@ -253,4 +258,82 @@ describe('CommentTextareaComponent', () => { imageFiles: null }); }); + + describe('handleInputChange', () => { + it('should update the content value if the text content changes', () => { + component.commentTextarea.nativeElement.textContent = 'New content'; + component.content.setValue('Old content'); + + component['handleInputChange'](); + + expect(component.content.value).toBe('New content'); + }); + + it('should call updateLinksInTextarea with the new text content', () => { + const updateLinksSpy = spyOn(component, 'updateLinksInTextarea'); + component.commentTextarea.nativeElement.textContent = 'New content'; + + component['handleInputChange'](); + + expect(updateLinksSpy).toHaveBeenCalledWith('New content'); + }); + }); + + describe('onCommentTextareaFocus', () => { + it('should call updateLinksInTextarea with trimmed current text', () => { + const updateLinksSpy = spyOn(component, 'updateLinksInTextarea'); + component.commentTextarea.nativeElement.textContent = ' Current text '; + component.onCommentTextareaFocus(); + + expect(updateLinksSpy).toHaveBeenCalledWith('Current text'); + }); + }); + + describe('onCommentTextareaBlur', () => { + it('should set the stripped text to the content value', () => { + component.commentTextarea.nativeElement.textContent = 'Blur text'; + component.onCommentTextareaBlur(); + + expect(component.content.value).toBe('Blur text'); + }); + + it('should call emitComment', () => { + const emitCommentSpy = spyOn(component, 'emitComment'); + component.onCommentTextareaBlur(); + + expect(emitCommentSpy).toHaveBeenCalled(); + }); + }); + + describe('updateLinksInTextarea', () => { + it('should sanitize and set innerHTML if a URL pattern is found', () => { + const renderLinksSpy = spyOn(component, 'renderLinks').and.returnValue('http://example.com'); + mockSanitizer.sanitize.and.returnValue('http://example.com'); + const initializeLinksSpy = spyOn(component, 'initializeLinkClickListeners'); + const testText = 'Check this link http://example.com'; + + component['updateLinksInTextarea'](testText); + + expect(renderLinksSpy).toHaveBeenCalledWith(testText); + expect(component.commentTextarea.nativeElement.innerHTML).toBe('http://example.com'); + expect(initializeLinksSpy).toHaveBeenCalledWith(component.commentTextarea.nativeElement); + }); + + describe('initializeLinkClickListeners', () => { + it('should open links in a new tab on click', () => { + const element = document.createElement('div'); + const link = document.createElement('a'); + link.href = 'http://example.com'; + link.textContent = 'example'; + element.appendChild(link); + + const openSpy = spyOn(window, 'open'); + component['initializeLinkClickListeners'](element); + + link.click(); + + expect(openSpy).toHaveBeenCalledWith('http://example.com', '_blank', 'noopener,noreferrer'); + }); + }); + }); });