diff --git a/src/app/components/shared/_dialogs/create-comment/create-comment.component.html b/src/app/components/shared/_dialogs/create-comment/create-comment.component.html index 6fa43925f1a14a6f207aca8da48814c6bc04c5e5..66e9ee29f13060e03cff1886d4b3b8122461b028 100644 --- a/src/app/components/shared/_dialogs/create-comment/create-comment.component.html +++ b/src/app/components/shared/_dialogs/create-comment/create-comment.component.html @@ -1,26 +1,26 @@ <ars-row ars-flex-box> <ars-row> <div class="lang-selection"> - <button class="lang-btn" mat-button (click)="select.open()" - matTooltip="{{ 'spacy-dialog.lang-button-hint' | translate }}" - matTooltipShowDelay="750"> - <i class="material-icons">language</i> - <span *ngIf="!(selectedLang === 'auto')"> - {{'spacy-dialog.' + (languagetoolService.mapLanguageToSpacyModel(selectedLang)) | translate}} - </span> - <span *ngIf="(selectedLang === 'auto')" id="langSelect"> + <button class="lang-btn" mat-button (click)="select.open()" + matTooltip="{{ 'spacy-dialog.lang-button-hint' | translate }}" + matTooltipShowDelay="750"> + <i class="material-icons">language</i> + <span *ngIf="!(grammarChecker.selectedLang === 'auto')"> + {{'spacy-dialog.' + (languagetoolService.mapLanguageToSpacyModel(grammarChecker.selectedLang)) | translate}} + </span> + <span *ngIf="(grammarChecker.selectedLang === 'auto')" id="langSelect"> auto - </span> - <mat-select class="select-list" #select [(ngModel)]="selectedLang"> - <mat-option *ngFor="let lang of languages" [value]="lang"> - <span *ngIf="lang == 'de-DE'">{{ 'spacy-dialog.de' | translate }}</span> - <span *ngIf="lang == 'en-US'">{{ 'spacy-dialog.en' | translate }}</span> - <span *ngIf="lang == 'fr'">{{ 'spacy-dialog.fr' | translate }}</span> - <span *ngIf="lang == 'auto'">{{ 'spacy-dialog.auto' | translate }}</span> - </mat-option> - </mat-select> - </button> - </div> + </span> + <mat-select class="select-list" #select [(ngModel)]="grammarChecker.selectedLang"> + <mat-option *ngFor="let lang of grammarChecker.languages" [value]="lang"> + <span *ngIf="lang == 'de-DE'">{{ 'spacy-dialog.de' | translate }}</span> + <span *ngIf="lang == 'en-US'">{{ 'spacy-dialog.en' | translate }}</span> + <span *ngIf="lang == 'fr'">{{ 'spacy-dialog.fr' | translate }}</span> + <span *ngIf="lang == 'auto'">{{ 'spacy-dialog.auto' | translate }}</span> + </mat-option> + </mat-select> + </button> + </div> <div class="anchor-wrp"> <div class="anchor-right"> <mat-form-field *ngIf="tags" class="tag-form-field"> @@ -47,8 +47,9 @@ <mat-form-field style="width:100%;"> <input [disabled]="true" matInput> <div + (document:click)="grammarChecker.onDocumentClick($event)" [contentEditable]="true" - (paste)="onPaste($event); maxLength(commentBody)" + (paste)="grammarChecker.onPaste($event); grammarChecker.maxLength(commentBody, user.role === 3 ? 1000 : 500)" [spellcheck]="false" (focus)="eventService.makeFocusOnInputTrue()" style="margin-top:15px;width:100%;" @@ -56,7 +57,7 @@ #commentBody aria-labelledby="ask-question-description" autofocus - (input)="maxLength(commentBody)" + (input)="grammarChecker.maxLength(commentBody, user.role === 3 ? 1000 : 500)" id="answer-input"> </div> <mat-placeholder class="placeholder"> @@ -69,10 +70,10 @@ </mat-hint> <mat-hint align="end"> <span aria-hidden="true"> - {{inputText.length}} / {{user.role === 3 ? 1000 : 500}} + {{commentBody.innerHTML.length}} / {{user.role === 3 ? 1000 : 500}} </span> </mat-hint> - <span *ngIf="!this.hasSpellcheckConfidence"> + <span *ngIf="!grammarChecker.hasSpellcheckConfidence"> <p class="lang-confidence">{{ 'spacy-dialog.force-language-selection' | translate }}</p> </span> @@ -88,7 +89,7 @@ <ars-row [height]="12"></ars-row> <ars-row> <markdown katex emoji lineNumbers lineHighlight - [data]="body"></markdown> + [data]="commentBody.innerText"></markdown> </ars-row> </mat-tab> </mat-tab-group> @@ -101,9 +102,9 @@ <button [disabled]="this.commentBody.innerHTML.length < 4 " mat-flat-button class="spell-button" - (click)="grammarCheck(commentBody)"> + (click)="grammarChecker.grammarCheck(commentBody)"> {{ 'comment-page.grammar-check' | translate}} - <mat-icon *ngIf="isSpellchecking" style="margin: 0;"> + <mat-icon *ngIf="grammarChecker.isSpellchecking" style="margin: 0;"> <mat-spinner diameter="20"></mat-spinner> </mat-icon> </button> @@ -112,7 +113,7 @@ <app-dialog-action-buttons buttonsLabelSection="comment-page" confirmButtonLabel="send" - [showLoadingCycle] = "isSendingToSpacy" + [showLoadingCycle]="isSendingToSpacy" [showDivider]="false" [spacing]="false" [cancelButtonClickAction]="buildCloseDialogActionCallback()" diff --git a/src/app/components/shared/_dialogs/create-comment/create-comment.component.ts b/src/app/components/shared/_dialogs/create-comment/create-comment.component.ts index 5ca59b3528a5543b164bdd09a603854fb9d0d20f..0b5c95145aa8241b5397a2be7e1baad629be586d 100644 --- a/src/app/components/shared/_dialogs/create-comment/create-comment.component.ts +++ b/src/app/components/shared/_dialogs/create-comment/create-comment.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, Inject, OnInit, ViewChild } from '@angular/core'; import { Comment, Language as CommentLanguage } from '../../../../models/comment'; import { NotificationService } from '../../../../services/util/notification.service'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; @@ -10,15 +10,16 @@ import { EventService } from '../../../../services/util/event.service'; import { SpacyDialogComponent } from '../spacy-dialog/spacy-dialog.component'; import { LanguagetoolService, Language } from '../../../../services/http/languagetool.service'; import { CreateCommentKeywords } from '../../../../utils/create-comment-keywords'; +import { GrammarChecker } from '../../../../utils/grammar-checker'; @Component({ selector: 'app-submit-comment', templateUrl: './create-comment.component.html', styleUrls: ['./create-comment.component.scss'] }) -export class CreateCommentComponent implements OnInit, OnDestroy { +export class CreateCommentComponent implements OnInit { - @ViewChild('commentBody', {static: true}) commentBody: HTMLDivElement; + @ViewChild('commentBody', { static: true }) commentBody: HTMLDivElement; comment: Comment; @@ -26,19 +27,11 @@ export class CreateCommentComponent implements OnInit, OnDestroy { roomId: string; tags: string[]; selectedTag: string; - inputText = ''; - body: string; - - languages: Language[] = ['de-DE', 'en-US', 'fr', 'auto']; - selectedLang: Language = 'auto'; bodyForm = new FormControl('', [Validators.required]); - isSpellchecking = false; isSendingToSpacy = false; - hasSpellcheckConfidence = true; - - newLang = 'auto'; + grammarChecker: GrammarChecker; constructor( private notification: NotificationService, @@ -49,49 +42,17 @@ export class CreateCommentComponent implements OnInit, OnDestroy { public languagetoolService: LanguagetoolService, public eventService: EventService, @Inject(MAT_DIALOG_DATA) public data: any) { + this.grammarChecker = new GrammarChecker(languagetoolService); } ngOnInit() { this.translateService.use(localStorage.getItem('currentLang')); - setTimeout(() => { - document.getElementById('answer-input').focus(); - document.addEventListener('click', this.onDocumentClick); - }, 0); - } - - onDocumentClick(e) { - const container = document.getElementsByClassName('dropdownBlock'); - Array.prototype.forEach.call(container, (elem) => { - if (!elem.contains(e.target) && (!(e.target as Node).parentElement.classList.contains('markUp') - || (e.target as HTMLElement).dataset.id !== ((elem as Node).parentElement as HTMLElement).dataset.id)) { - (elem as HTMLElement).style.display = 'none'; - } - }); - } - - ngOnDestroy() { - document.removeEventListener('click', this.onDocumentClick); } onNoClick(): void { this.dialogRef.close(); } - onPaste(e) { - e.preventDefault(); - const elem = document.getElementById('answer-input'); - const text = e.clipboardData.getData('text'); - elem.innerText += text.replace(/<[^>]*>?/gm, ''); - - const range = document.createRange(); - range.setStart(elem.lastChild, elem.lastChild.textContent.length); - range.collapse(true); - - const sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); - } - checkInputData(body: string): boolean { body = body.trim(); if (!body) { @@ -117,13 +78,14 @@ export class CreateCommentComponent implements OnInit, OnDestroy { } openSpacyDialog(comment: Comment): void { - CreateCommentKeywords.isSpellingAcceptable(this.languagetoolService, this.inputText, this.selectedLang) + CreateCommentKeywords.isSpellingAcceptable(this.languagetoolService, this.commentBody.innerHTML, this.grammarChecker.selectedLang) .subscribe((result) => { if (result.isAcceptable) { const commentLang = this.languagetoolService.mapLanguageToSpacyModel(result.result.language.code as Language); - const selectedLangExtend = this.selectedLang[2] === '-' ? this.selectedLang.substr(0, 2) : this.selectedLang; + const selectedLangExtend = this.grammarChecker.selectedLang[2] === '-' ? + this.grammarChecker.selectedLang.substr(0, 2) : this.grammarChecker.selectedLang; // Store language if it was auto-detected - if (this.selectedLang === 'auto') { + if (this.grammarChecker.selectedLang === 'auto') { comment.language = Comment.mapModelToLanguage(commentLang); } else if (CommentLanguage[selectedLangExtend]) { comment.language = CommentLanguage[selectedLangExtend]; @@ -161,128 +123,4 @@ export class CreateCommentComponent implements OnInit, OnDestroy { buildCreateCommentActionCallback(text: HTMLDivElement): () => void { return () => this.closeDialog(text.innerText); } - - checkSpellings(text: string, language: Language = this.selectedLang) { - return this.languagetoolService.checkSpellings(CreateCommentKeywords.cleaningFunction(text), language); - } - - maxLength(commentBody: HTMLDivElement): void { - if (this.user.role === 3 && commentBody.innerText.length > 1000) { - commentBody.innerText = commentBody.innerText.slice(0, 1000); - } else if (this.user.role !== 3 && commentBody.innerText.length > 500) { - commentBody.innerText = commentBody.innerText.slice(0, 500); - } - this.body = commentBody.innerText; - if (this.body.length === 1 && this.body.charCodeAt(this.body.length - 1) === 10) { - commentBody.innerHTML = commentBody.innerHTML.replace('<br>', ''); - } - this.inputText = commentBody.innerText; - } - - grammarCheck(commentBody: HTMLDivElement): void { - const wrongWords: string[] = []; - commentBody.innerHTML = this.inputText; - this.isSpellchecking = true; - this.hasSpellcheckConfidence = true; - this.checkSpellings(commentBody.innerText).subscribe((wordsCheck) => { - if (!this.checkLanguageConfidence(wordsCheck)) { - this.hasSpellcheckConfidence = false; - return; - } - if (this.selectedLang === 'auto' && (document.getElementById('langSelect').innerText.includes(this.newLang) - || document.getElementById('langSelect').innerText.includes('auto'))) { - if (wordsCheck.language.name.includes('German')) { - this.selectedLang = 'de-DE'; - } else if (wordsCheck.language.name.includes('English')) { - this.selectedLang = 'en-US'; - } else if (wordsCheck.language.name.includes('French')) { - this.selectedLang = 'fr'; - } else { - this.newLang = wordsCheck.language.name; - } - document.getElementById('langSelect').innerHTML = this.newLang; - } - if (wordsCheck.matches.length > 0) { - wordsCheck.matches.forEach(grammarError => { - const wrongWord = commentBody.innerText.slice(grammarError.offset, grammarError.offset + grammarError.length); - wrongWords.push(wrongWord); - }); - - this.checkSpellings(commentBody.innerHTML).subscribe((res) => { - for (let i = res.matches.length - 1; i >= 0; i--) { - const wrongWord = commentBody.innerHTML - .slice(res.matches[i].offset, res.matches[i].offset + res.matches[i].length); - - if (wrongWords.includes(wrongWord)) { - const suggestions: any[] = res.matches[i].replacements; - let displayOptions = 3; - let suggestionsHTML = ''; - - if (!suggestions.length) { - suggestionsHTML = '<span style="color: black; display: block; text-align: center;">' + res.matches[i].message + '</span>'; - } - - if (suggestions.length < displayOptions) { - displayOptions = suggestions.length; - } - - for (let j = 0; j < displayOptions; j++) { - // eslint-disable-next-line max-len - suggestionsHTML += '<span class="suggestions"' + ' style="color: black; display: block; text-align: center; cursor: pointer;">' + suggestions[j].value + '</span>'; - } - - const replacement = - '<div class="markUp" data-id="'+i+'" style="position: relative; display: inline-block; border-bottom: 1px dotted black">' + - '<span data-id="' + i + '" style="text-decoration: underline wavy red; cursor: pointer;">' + - wrongWord + - '</span>' + - // eslint-disable-next-line max-len - '<div class="dropdownBlock" style="display: none; width: 160px; background-color: white; border-style: solid; border-color: var(--primary); color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1000; bottom: 100%;">' + - suggestionsHTML + - '</div>' + - '</div>'; - - commentBody.innerHTML = commentBody.innerHTML.substr(0, res.matches[i].offset) + - replacement + commentBody.innerHTML.substr(res.matches[i].offset + wrongWord.length, commentBody.innerHTML.length); - } - } - - setTimeout(() => { - Array.from(document.getElementsByClassName('markUp')).forEach(markup => { - markup.addEventListener('click', () => { - ((markup as HTMLElement).lastChild as HTMLElement).style.display = 'block'; - const rectdiv = (document.getElementById('answer-input')).getBoundingClientRect(); - const rectmarkup = markup.getBoundingClientRect(); - let offset; - if (rectmarkup.x + rectmarkup.width / 2 > rectdiv.right - 80) { - offset = rectdiv.right - rectmarkup.x - rectmarkup.width; - ((markup as HTMLElement).lastChild as HTMLElement).style.right = -offset + 'px'; - } else if (rectmarkup.x + rectmarkup.width / 2 < rectdiv.left + 80) { - offset = rectmarkup.x - rectdiv.left; - ((markup as HTMLElement).lastChild as HTMLElement).style.left = -offset + 'px'; - } else { - ((markup as HTMLElement).lastChild as HTMLElement).style.left = '50%'; - ((markup as HTMLElement).lastChild as HTMLElement).style.marginLeft = '-80px'; - } - setTimeout(() => { - Array.from(document.getElementsByClassName('suggestions')).forEach(suggestion => { - suggestion.addEventListener('click', () => { - suggestion.parentElement.parentElement.outerHTML = suggestion.innerHTML; - this.inputText = commentBody.innerText; - }); - }); - }, 500); - }); - }); - }, 500); - }); - } - }, () => '', () => { - this.isSpellchecking = false; - }); - } - - checkLanguageConfidence(wordsCheck: any) { - return this.selectedLang === 'auto' ? wordsCheck.language.detectedLanguage.confidence >= 0.5 : true; - } } diff --git a/src/app/components/shared/comment-answer/comment-answer.component.html b/src/app/components/shared/comment-answer/comment-answer.component.html index e96bc84b6a25d5cb7bc5c7f4b48b67822a3bfa75..2036f274b2b7e12187afa36deb4ec795f82fa2ce 100644 --- a/src/app/components/shared/comment-answer/comment-answer.component.html +++ b/src/app/components/shared/comment-answer/comment-answer.component.html @@ -12,63 +12,107 @@ *ngIf="!isStudent || answer"> <div *ngIf="(isStudent || !edit) && answer"> <markdown class="images" katex emoji lineNumbers lineHighlight - [data]="answer"></markdown> + [data]="answer"></markdown> <div fxLayout="row" fxLayoutAlign="end"> <button mat-raised-button *ngIf="!isStudent && !edit" class="save" - (click)="edit = true"> + (click)="onEditClick()"> <mat-icon>edit</mat-icon> {{'comment-page.edit-answer' | translate}} </button> </div> </div> - <mat-tab-group *ngIf="!isStudent && (edit || !answer)" - [dynamicHeight]="false"> - <mat-tab label="{{'comment-page.your-answer' | translate}}"> - <mat-form-field class="input-block"> - <textarea [(ngModel)]="answer" - (input)="edit = true" - (keyup)="$event.cancelBubble=true" - matInput - matTextareaAutosize - matAutosizeMinRows="3" - matAutosizeMaxRows="10" - maxlength="2000" - name="answer" - autofocus> - </textarea> - <mat-hint align="start"> + <div class="lang-selection" + *ngIf="!isStudent && (edit || !answer)"> + <button class="lang-btn" mat-button (click)="select.open()" + matTooltip="{{ 'spacy-dialog.lang-button-hint' | translate }}" + matTooltipShowDelay="750"> + <i class="material-icons">language</i> + <span *ngIf="!(grammarChecker.selectedLang === 'auto')"> + {{'spacy-dialog.' + (languagetoolService.mapLanguageToSpacyModel(grammarChecker.selectedLang)) | translate}} + </span> + <span *ngIf="(grammarChecker.selectedLang === 'auto')" id="langSelect"> + auto + </span> + <mat-select class="select-list" #select [(ngModel)]="grammarChecker.selectedLang"> + <mat-option *ngFor="let lang of grammarChecker.languages" [value]="lang"> + <span *ngIf="lang == 'de-DE'">{{ 'spacy-dialog.de' | translate }}</span> + <span *ngIf="lang == 'en-US'">{{ 'spacy-dialog.en' | translate }}</span> + <span *ngIf="lang == 'fr'">{{ 'spacy-dialog.fr' | translate }}</span> + <span *ngIf="lang == 'auto'">{{ 'spacy-dialog.auto' | translate }}</span> + </mat-option> + </mat-select> + </button> + </div> + <div *ngIf="!isStudent && (edit || !answer)"> + <mat-tab-group [dynamicHeight]="false"> + <mat-tab label="{{'comment-page.your-answer' | translate}}"> + <mat-divider></mat-divider> + <mat-form-field class="input-block"> + <input [disabled]="true" matInput> + <div + (document:click)="grammarChecker.onDocumentClick($event)" + [contentEditable]="true" + (paste)="grammarChecker.onPaste($event); grammarChecker.maxLength(commentBody, 2000)" + [spellcheck]="false" + (focus)="eventService.makeFocusOnInputTrue()" + style="margin-top:1.25em; width:100%;" + (blur)="eventService.makeFocusOnInputFalse()" + #commentBody + aria-labelledby="ask-question-description" + autofocus + (input)="grammarChecker.maxLength(commentBody, 2000); answer = commentBody.innerText" + id="answer-input"> + </div> + <mat-hint align="start"> <span aria-hidden="true"> {{ 'comment-page.Markdown-hint' | translate }} </span> - </mat-hint> - <mat-hint align="end"> + </mat-hint> + <mat-hint align="end"> <span aria-hidden="true"> {{ answer ? answer.length : 0 }} / 2000 </span> - </mat-hint> - </mat-form-field> - </mat-tab> - <mat-tab label="{{'session.preview' | translate}}" - [disabled]="!answer"> - <markdown class="images" katex emoji lineNumbers lineHighlight - [data]="answer"></markdown> - </mat-tab> - </mat-tab-group> - <div fxLayout="row" - fxLayoutAlign="end"> - <button mat-raised-button - *ngIf="!isStudent && answer && edit" - class="delete" - (click)="openDeleteAnswerDialog()"> - {{'comment-page.delete-answer' | translate}}</button> - <button mat-raised-button - *ngIf="!isStudent && (edit || !answer)" - class="save" - (click)="saveAnswer()"> - {{'comment-page.save-answer' | translate}}</button> + </mat-hint> + </mat-form-field> + </mat-tab> + <mat-tab label="{{'session.preview' | translate}}" + [disabled]="!answer"> + <markdown class="images" katex emoji lineNumbers lineHighlight + [data]="answer"></markdown> + </mat-tab> + </mat-tab-group> + <div id="buttonWrapper" + fxLayout="col"> + <div fxLayout="row"> + <button + *ngIf="edit" + [disabled]="commentBody.innerHTML.length < 4 " + mat-raised-button + (click)="grammarChecker.grammarCheck(commentBody)" + class="spell-button"> + {{ 'comment-page.grammar-check' | translate}} + <mat-icon *ngIf="grammarChecker.isSpellchecking" style="margin: 0;"> + <mat-spinner diameter="20"></mat-spinner> + </mat-icon> + </button> + </div> + <div fxLayout="row"> + <button mat-raised-button + *ngIf="answer" + class="delete" + style="display: inline-block" + (click)="openDeleteAnswerDialog()"> + {{'comment-page.delete-answer' | translate}}</button> + <button mat-raised-button + class="save" + style="display: inline-block" + (click)="answer = commentBody.innerText; saveAnswer()"> + {{'comment-page.save-answer' | translate}}</button> + </div> + </div> </div> </mat-card> </div> diff --git a/src/app/components/shared/comment-answer/comment-answer.component.scss b/src/app/components/shared/comment-answer/comment-answer.component.scss index b87b500b6b06a4fe23dc4d3fdb661110396419cb..b11d9ee32e57c8109e9c25e47bddb2fe8b166aad 100644 --- a/src/app/components/shared/comment-answer/comment-answer.component.scss +++ b/src/app/components/shared/comment-answer/comment-answer.component.scss @@ -45,3 +45,57 @@ mat-icon { .border-answer { box-shadow: 0 2px 1px -1px rgba(0, 0, 0, .2), 0 1px 1px 0 rgba(0, 0, 0, .14), 0 1px 3px 0 rgba(0, 0, 0, .12), -4px 0 0 0 var(--primary); } + +.material-icons { + margin-right: 18px; +} + +.select-list { + width: calc(100% - 24px); +} + +.lang-selection { + vertical-align: middle; + margin-right: 0; +} + +::ng-deep .mat-select-value { + width: auto !important; +} + +::ng-deep .mat-select-arrow-wrapper .mat-select-arrow { + color: var(--on-surface); + margin-right: 50px; +} + +::ng-deep .mat-select-panel { + background: var(--dialog); +} + +.mat-option { + color: var(--on-surface); +} + +.spell-button{ + background-color: var(--primary); + color: var(--on-primary); + display: inline-block +} + +#buttonWrapper { + justify-content: space-between; +} + +#answer-input { + line-height: 120%; + color: var(--on-surface); + caret-color: var(--on-surface); + -webkit-appearance: textarea; + min-height: 50px; + cursor: text; + word-wrap: break-word; + + &:focus { + outline: none; + } +} diff --git a/src/app/components/shared/comment-answer/comment-answer.component.ts b/src/app/components/shared/comment-answer/comment-answer.component.ts index c4641fc14f04fdac912d98ff94a15a935f9161e2..aa1fcfbbed294460005e9c9e9729ee28a151d37d 100644 --- a/src/app/components/shared/comment-answer/comment-answer.component.ts +++ b/src/app/components/shared/comment-answer/comment-answer.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { LanguageService } from '../../../services/util/language.service'; @@ -11,7 +11,9 @@ import { UserRole } from '../../../models/user-roles.enum'; import { NotificationService } from '../../../services/util/notification.service'; import { MatDialog } from '@angular/material/dialog'; import { DeleteAnswerComponent } from '../../creator/_dialogs/delete-answer/delete-answer.component'; -import { RoomDataService } from '../../../services/util/room-data.service'; +import { LanguagetoolService } from '../../../services/http/languagetool.service'; +import { GrammarChecker } from '../../../utils/grammar-checker'; +import { EventService } from '../../../services/util/event.service'; @Component({ selector: 'app-comment-answer', @@ -27,6 +29,10 @@ export class CommentAnswerComponent implements OnInit { isStudent = true; edit = false; + grammarChecker: GrammarChecker; + + @ViewChild('commentBody') commentBody: ElementRef<HTMLDivElement>; + constructor(protected route: ActivatedRoute, private notificationService: NotificationService, private translateService: TranslateService, @@ -34,8 +40,11 @@ export class CommentAnswerComponent implements OnInit { protected wsCommentService: WsCommentService, protected commentService: CommentService, private authenticationService: AuthenticationService, - private roomDataService: RoomDataService, - public dialog: MatDialog) { } + public languagetoolService: LanguagetoolService, + public dialog: MatDialog, + eventService: EventService) { + this.grammarChecker = new GrammarChecker(languagetoolService); + } ngOnInit() { this.user = this.authenticationService.getUser(); @@ -46,13 +55,14 @@ export class CommentAnswerComponent implements OnInit { this.commentService.getComment(params['commentId']).subscribe(comment => { this.comment = comment; this.answer = this.comment.answer; + this.edit = !this.answer; this.isLoading = false; }); }); } saveAnswer() { - this.edit = false; + this.edit = !this.answer; this.commentService.answer(this.comment, this.answer).subscribe(); this.translateService.get('comment-page.comment-answered').subscribe(msg => { this.notificationService.show(msg); @@ -72,10 +82,20 @@ export class CommentAnswerComponent implements OnInit { } deleteAnswer() { + if (this.commentBody) { + this.commentBody.nativeElement.innerText = ''; + } this.answer = null; this.commentService.answer(this.comment, this.answer).subscribe(); this.translateService.get('comment-page.answer-deleted').subscribe(msg => { this.notificationService.show(msg); }); } + + onEditClick() { + this.edit = true; + setTimeout(() => { + this.commentBody.nativeElement.innerText = this.answer; + }); + } } diff --git a/src/app/components/shared/shared.module.ts b/src/app/components/shared/shared.module.ts index 9cd33abc6679160ee477ec020816bac3c1049885..557d35a5d9f501ef0aa5090271e11d9faf0c4fd0 100644 --- a/src/app/components/shared/shared.module.ts +++ b/src/app/components/shared/shared.module.ts @@ -40,7 +40,7 @@ import { TagCloudPopUpComponent } from './tag-cloud/tag-cloud-pop-up/tag-cloud-p import { WorkerDialogComponent } from './_dialogs/worker-dialog/worker-dialog.component'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { ActiveUserComponent } from './overlay/active-user/active-user.component'; -import { AutofocusDirective } from '../../directive/autofocus.directive'; +import { AutofocusDirective } from '../../directives/autofocus.directive'; @NgModule({ imports: [ diff --git a/src/app/directive/autofocus.directive.spec.ts b/src/app/directives/autofocus.directive.spec.ts similarity index 100% rename from src/app/directive/autofocus.directive.spec.ts rename to src/app/directives/autofocus.directive.spec.ts diff --git a/src/app/directive/autofocus.directive.ts b/src/app/directives/autofocus.directive.ts similarity index 100% rename from src/app/directive/autofocus.directive.ts rename to src/app/directives/autofocus.directive.ts diff --git a/src/app/utils/grammar-checker.ts b/src/app/utils/grammar-checker.ts new file mode 100644 index 0000000000000000000000000000000000000000..2524e73a92c2ce9e627cef2574f747b58b3aeb2f --- /dev/null +++ b/src/app/utils/grammar-checker.ts @@ -0,0 +1,163 @@ +import { Language, LanguagetoolService } from '../services/http/languagetool.service'; +import { CreateCommentKeywords } from './create-comment-keywords'; + +export class GrammarChecker { + + languages: Language[] = ['de-DE', 'en-US', 'fr', 'auto']; + selectedLang: Language = 'auto'; + isSpellchecking = false; + hasSpellcheckConfidence = true; + newLang = 'auto'; + + constructor(private languagetoolService: LanguagetoolService) { + } + + onDocumentClick(e) { + const container = document.getElementsByClassName('dropdownBlock'); + Array.prototype.forEach.call(container, (elem) => { + if (!elem.contains(e.target) && (!(e.target as Node).parentElement.classList.contains('markUp') + || (e.target as HTMLElement).dataset.id !== ((elem as Node).parentElement as HTMLElement).dataset.id)) { + (elem as HTMLElement).style.display = 'none'; + } + }); + } + + maxLength(commentBody: HTMLDivElement, size: number): void { + if (commentBody.innerText.length > size) { + commentBody.innerText = commentBody.innerText.slice(0, size); + } + const body = commentBody.innerText; + if (body.length === 1 && body.charCodeAt(body.length - 1) === 10) { + commentBody.innerHTML = commentBody.innerHTML.replace('<br>', ''); + } + } + + onPaste(e) { + e.preventDefault(); + const elem = document.getElementById('answer-input'); + const text = e.clipboardData.getData('text'); + elem.innerText += text.replace(/<[^>]*>?/gm, ''); + + const range = document.createRange(); + range.setStart(elem.lastChild, elem.lastChild.textContent.length); + range.collapse(true); + + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + + grammarCheck(commentBody: HTMLDivElement): void { + const wrongWords: string[] = []; + this.isSpellchecking = true; + this.hasSpellcheckConfidence = true; + const text = CreateCommentKeywords.cleaningFunction(commentBody.innerText); + this.checkSpellings(text).subscribe((wordsCheck) => { + if (!this.checkLanguageConfidence(wordsCheck)) { + this.hasSpellcheckConfidence = false; + return; + } + if (this.selectedLang === 'auto' && (document.getElementById('langSelect').innerText.includes(this.newLang) + || document.getElementById('langSelect').innerText.includes('auto'))) { + if (wordsCheck.language.name.includes('German')) { + this.selectedLang = 'de-DE'; + } else if (wordsCheck.language.name.includes('English')) { + this.selectedLang = 'en-US'; + } else if (wordsCheck.language.name.includes('French')) { + this.selectedLang = 'fr'; + } else { + this.newLang = wordsCheck.language.name; + } + document.getElementById('langSelect').innerHTML = this.newLang; + } + if (wordsCheck.matches.length > 0) { + wordsCheck.matches.forEach(grammarError => { + const wrongWord = commentBody.innerText.slice(grammarError.offset, grammarError.offset + grammarError.length); + wrongWords.push(wrongWord); + }); + + let html = ''; + let lastFound = text.length; + this.checkSpellings(text).subscribe((res) => { + for (let i = res.matches.length - 1; i >= 0; i--) { + const end = res.matches[i].offset + res.matches[i].length; + const start = res.matches[i].offset; + const wrongWord = text.slice(start, end); + + if (wrongWords.includes(wrongWord)) { + const suggestions: any[] = res.matches[i].replacements; + let displayOptions = 3; + let suggestionsHTML = ''; + + if (!suggestions.length) { + suggestionsHTML = '<span style="color: black; display: block; text-align: center;">' + res.matches[i].message + '</span>'; + } + + if (suggestions.length < displayOptions) { + displayOptions = suggestions.length; + } + + for (let j = 0; j < displayOptions; j++) { + // eslint-disable-next-line max-len + suggestionsHTML += '<span class="suggestions"' + ' style="color: black; display: block; text-align: center; cursor: pointer;">' + suggestions[j].value + '</span>'; + } + + const replacement = + '<div class="markUp" data-id="' + i + '" style="position: relative; display: inline-block; border-bottom: 1px dotted black">' + + '<span data-id="' + i + '" style="text-decoration: underline wavy red; cursor: pointer;">' + + wrongWord + + '</span>' + + // eslint-disable-next-line max-len + '<div class="dropdownBlock" style="display: none; width: 160px; background-color: white; border-style: solid; border-color: var(--primary); color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1000; bottom: 100%;">' + + suggestionsHTML + + '</div>' + + '</div>'; + + html = replacement + text.slice(end, lastFound) + html; + lastFound = res.matches[i].offset; + } + } + commentBody.innerHTML = text.slice(0, lastFound) + html; + + setTimeout(() => { + Array.from(document.getElementsByClassName('markUp')).forEach(markup => { + markup.addEventListener('click', () => { + ((markup as HTMLElement).lastChild as HTMLElement).style.display = 'block'; + const rectdiv = (document.getElementById('answer-input')).getBoundingClientRect(); + const rectmarkup = markup.getBoundingClientRect(); + let offset; + if (rectmarkup.x + rectmarkup.width / 2 > rectdiv.right - 80) { + offset = rectdiv.right - rectmarkup.x - rectmarkup.width; + ((markup as HTMLElement).lastChild as HTMLElement).style.right = -offset + 'px'; + } else if (rectmarkup.x + rectmarkup.width / 2 < rectdiv.left + 80) { + offset = rectmarkup.x - rectdiv.left; + ((markup as HTMLElement).lastChild as HTMLElement).style.left = -offset + 'px'; + } else { + ((markup as HTMLElement).lastChild as HTMLElement).style.left = '50%'; + ((markup as HTMLElement).lastChild as HTMLElement).style.marginLeft = '-80px'; + } + setTimeout(() => { + Array.from(document.getElementsByClassName('suggestions')).forEach(suggestion => { + suggestion.addEventListener('click', () => { + suggestion.parentElement.parentElement.outerHTML = suggestion.innerHTML; + }); + }); + }, 500); + }); + }); + }, 500); + }); + } + }, () => '', () => { + this.isSpellchecking = false; + }); + } + + checkLanguageConfidence(wordsCheck: any) { + return this.selectedLang === 'auto' ? wordsCheck.language.detectedLanguage.confidence >= 0.5 : true; + } + + checkSpellings(text: string, language: Language = this.selectedLang) { + return this.languagetoolService.checkSpellings(text, language); + } +}