Skip to content
Snippets Groups Projects
Commit ce4f540f authored by Ruben Bimberg's avatar Ruben Bimberg :computer:
Browse files

Implement wysiwg editor (partial)

A partial commit to the upcoming meeting.
parent 0b1b68b1
Branches
Tags
No related merge requests found
......@@ -26,7 +26,10 @@
"./node_modules/material-design-icons/iconfont/material-icons.css",
"node_modules/prismjs/themes/prism-okaidia.css",
"node_modules/prismjs/plugins/line-highlight/prism-line-highlight.css",
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css"
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css",
"node_modules/quill/dist/quill.core.css",
"node_modules/quill/dist/quill.bubble.css",
"node_modules/quill/dist/quill.snow.css"
],
"scripts": [
"node_modules/marked/lib/marked.js",
......@@ -36,7 +39,8 @@
"node_modules/prismjs/components/prism-css.min.js",
"node_modules/prismjs/plugins/line-highlight/prism-line-highlight.js",
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js",
"node_modules/katex/dist/katex.min.js"
"node_modules/katex/dist/katex.min.js",
"node_modules/quill/dist/quill.js"
]
},
"configurations": {
......
source diff could not be displayed: it is too large. Options to address this: view the blob.
......@@ -47,7 +47,9 @@
"ngx-markdown": "^11.1.3",
"ngx-matomo": "^0.1.4",
"ngx-matomo-v9": "^0.3.0",
"ngx-quill": "^14.2.0",
"prismjs": "^1.23.0",
"quill": "^1.3.7",
"rxjs": "^6.5.4",
"tslib": "^2.0.0",
"typescript-map": "0.0.7",
......
......@@ -63,6 +63,7 @@ import { TagCloudModule } from 'angular-tag-cloud-module';
import { SpacyService } from './services/http/spacy.service';
import { QuizNowComponent } from './components/shared/quiz-now/quiz-now.component';
import { JoyrideModule } from 'ngx-joyride';
import { QuillModule } from 'ngx-quill';
import 'prismjs';
import 'prismjs/plugins/line-numbers/prism-line-numbers.js';
......@@ -145,7 +146,27 @@ export function initializeApp(appConfig: AppConfig) {
}),
ArsModule,
TagCloudModule,
JoyrideModule.forRoot()
JoyrideModule.forRoot(),
QuillModule.forRoot({
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ script: 'sub' }, { script: 'super' }],
[{ indent: '-1' }, { indent: '+1' }],
[{ direction: 'rtl' }],
[{ size: ['small', false, 'large', 'huge'] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
['clean'],
['link', 'image', 'video']
]
}
})
],
providers: [
/*AppConfig,
......
import { Component, Inject, OnInit } 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';
......@@ -9,7 +9,7 @@ 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';
import { WriteCommentComponent } from '../../write-comment/write-comment.component';
@Component({
selector: 'app-submit-comment',
......@@ -18,13 +18,13 @@ import { GrammarChecker } from '../../../../utils/grammar-checker';
})
export class CreateCommentComponent implements OnInit {
@ViewChild(WriteCommentComponent) commentComponent: WriteCommentComponent;
comment: Comment;
user: User;
roomId: string;
tags: string[];
selectedTag: string;
isSendingToSpacy = false;
grammarChecker: GrammarChecker;
tempEditView: string;
constructor(
......@@ -35,7 +35,6 @@ export class CreateCommentComponent implements OnInit {
public languagetoolService: LanguagetoolService,
public eventService: EventService,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.grammarChecker = new GrammarChecker(this.languagetoolService);
}
ngOnInit() {
......@@ -71,14 +70,14 @@ export class CreateCommentComponent implements OnInit {
}
openSpacyDialog(comment: Comment): void {
CreateCommentKeywords.isSpellingAcceptable(this.languagetoolService, comment.body, this.grammarChecker.selectedLang)
CreateCommentKeywords.isSpellingAcceptable(this.languagetoolService, comment.body, this.commentComponent.selectedLang)
.subscribe((result) => {
if (result.isAcceptable) {
const commentLang = this.languagetoolService.mapLanguageToSpacyModel(result.result.language.code as Language);
const selectedLangExtend = this.grammarChecker.selectedLang[2] === '-' ?
this.grammarChecker.selectedLang.substr(0, 2) : this.grammarChecker.selectedLang;
const selectedLangExtend = this.commentComponent.selectedLang[2] === '-' ?
this.commentComponent.selectedLang.substr(0, 2) : this.commentComponent.selectedLang;
// Store language if it was auto-detected
if (this.grammarChecker.selectedLang === 'auto') {
if (this.commentComponent.selectedLang === 'auto') {
comment.language = Comment.mapModelToLanguage(commentLang);
} else if (CommentLanguage[selectedLangExtend]) {
comment.language = CommentLanguage[selectedLangExtend];
......
......@@ -80,13 +80,11 @@ export class CommentAnswerComponent implements OnInit {
this.deleteAnswer();
}
});
}
};
}
deleteAnswer() {
if (this.commentComponent.commentBody) {
this.commentComponent.commentBody.nativeElement.innerText = '';
}
this.commentComponent.clearHTML();
this.answer = null;
this.commentService.answer(this.comment, this.answer).subscribe();
this.translateService.get('comment-page.answer-deleted').subscribe(msg => {
......@@ -96,8 +94,6 @@ export class CommentAnswerComponent implements OnInit {
onEditClick() {
this.edit = true;
setTimeout(() => {
this.commentComponent.commentBody.nativeElement.innerText = this.answer;
});
setTimeout(() => this.commentComponent.setHTML(this.answer));
}
}
......@@ -49,6 +49,7 @@ import { MatSpinnerOverlayComponent } from './mat-spinner-overlay/mat-spinner-ov
import { WriteCommentComponent } from './write-comment/write-comment.component';
import { CustomMarkdownComponent } from './custom-markdown/custom-markdown.component';
import { ScrollIntoViewDirective } from '../../directives/scroll-into-view.directive';
import { QuillModule } from 'ngx-quill';
@NgModule({
imports: [
......@@ -62,7 +63,8 @@ import { ScrollIntoViewDirective } from '../../directives/scroll-into-view.direc
TagCloudModule,
ColorPickerModule,
DragDropModule,
JoyrideModule.forChild()
JoyrideModule.forChild(),
QuillModule
],
declarations: [
RoomJoinComponent,
......
......@@ -7,18 +7,14 @@
matTooltip="{{ 'spacy-dialog.lang-button-hint' | translate }}"
matTooltipShowDelay="750">
<mat-icon id="langSymbol">language</mat-icon>
<span *ngIf="!(grammarChecker.selectedLang === 'auto')">
{{'spacy-dialog.' + (languagetoolService.mapLanguageToSpacyModel(grammarChecker.selectedLang)) | translate}}
</span>
<span *ngIf="(grammarChecker.selectedLang === 'auto')"
#langSelect>
auto
</span>
<mat-select class="select-list"
#select
[(ngModel)]="grammarChecker.selectedLang">
<mat-option *ngFor="let lang of grammarChecker.languages"
[value]="lang">
<span *ngIf="selectedLang !== 'auto'">
{{'spacy-dialog.' + (languagetoolService.mapLanguageToSpacyModel(selectedLang)) | translate}}
</span>
<span *ngIf="selectedLang === 'auto'" #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>
......@@ -47,79 +43,30 @@
</mat-form-field>
</div>
</div>
<mat-tab-group (selectedTabChange)="onTabChange()" *ngIf="enabled">
<mat-tab label="{{ 'comment-page.write-comment' | translate }}">
<ars-row [height]="12"></ars-row>
<ars-row>
</ars-row>
<ars-row [height]="12"></ars-row>
<ars-row [overflow]="'visible'"
class="comment-write-container">
<mat-form-field class="full-width">
<input [disabled]="true"
matInput>
<div
(document:click)="grammarChecker.onDocumentClick($event)"
[contentEditable]="true"
(paste)="grammarChecker.onPaste($event); grammarChecker.maxLength(commentBody, user.role === 3 ? 1000 : 500)"
[spellcheck]="false"
(focus)="eventService.makeFocusOnInputTrue()"
(blur)="eventService.makeFocusOnInputFalse()"
#commentBody
aria-labelledby="ask-question-description"
autofocus
(input)="grammarChecker.maxLength(commentBody, user.role === 3 ? 1000 : 500)"
id="answer-input">
</div>
<mat-placeholder class="placeholder">
{{ 'comment-page.enter-comment' | translate }}
</mat-placeholder>
<mat-hint align="start">
<span aria-hidden="true">
{{ 'comment-page.Markdown-hint' | translate }}
</span>
</mat-hint>
<mat-hint align="end">
<span aria-hidden="true">
{{commentBody.innerText.length}} / {{user.role === 3 ? 1000 : 500}}
</span>
</mat-hint>
<span *ngIf="!grammarChecker.hasSpellcheckConfidence">
<p class="lang-confidence">{{ 'spacy-dialog.force-language-selection' | translate }}</p>
</span>
</mat-form-field>
</ars-row>
</mat-tab>
<mat-tab label="{{ 'comment-page.preview-comment' | translate }}"
[disabled]="!commentBody.innerText">
<ars-row [height]="12"></ars-row>
<ars-row>
</ars-row>
<ars-row [height]="12"></ars-row>
<ars-row>
<app-custom-markdown [data]="tempEditView"></app-custom-markdown>
</ars-row>
</mat-tab>
</mat-tab-group>
</ars-row>
<ars-row class="filler-row">
<ars-row [height]="12"></ars-row>
<ars-row *ngIf="enabled">
<div #editorErrorLayer style="z-index: -1;"></div>
<quill-editor #editor [maxLength]="10" placeholder="{{ 'comment-page.enter-comment' | translate }}">
</quill-editor>
<div fxLayout="row" style="justify-content: space-between; padding: 0 5px">
<span aria-hidden="true" style="font-size: 75%">
{{ 'comment-page.Markdown-hint' | translate }}
</span>
<span aria-hidden="true" style="font-size: 75%">
{{currentHTML.length}} / {{user.role === 3 ? 1000 : 500}}
</span>
</div>
</ars-row>
<ars-row ars-flex-box
*ngIf="enabled"
class="spellcheck">
<ars-row ars-flex-box *ngIf="enabled" class="spellcheck">
<ars-col>
<button
[disabled]="this.commentBody && this.commentBody.nativeElement.innerHTML.length < 4 "
[disabled]="currentText.length < 4"
mat-flat-button
class="spell-button"
(click)="grammarChecker.grammarCheck(commentBody.nativeElement)">
(click)="grammarCheck(currentText, langSelect && langSelect.nativeElement)">
{{ 'comment-page.grammar-check' | translate}}
<mat-icon *ngIf="grammarChecker.isSpellchecking"
class="spinner-container">
<mat-icon *ngIf="isSpellchecking" class="spinner-container">
<app-mat-spinner-overlay diameter="20" strokeWidth="2" [color]="'on-primary'"></app-mat-spinner-overlay>
</mat-icon>
</button>
......
......@@ -126,7 +126,8 @@ Styling for tag selection
}
.anchor-wrp {
width: 100%;
width: calc(100% - 200px);
display: inline-block;
height: 0;
position: relative;
left: 0;
......@@ -138,16 +139,19 @@ Styling for language select
*/
#langSymbol {
margin-right: 18px;
margin-right: 12px;
}
.select-list {
width: calc(100% - 24px);
width: 18px;
margin-left: 12px;
}
.lang-selection {
vertical-align: middle;
margin-right: 0;
width: 200px;
display: inline-block;
}
/*
......@@ -157,7 +161,6 @@ Styling for tag selection and language selection
::ng-deep {
.mat-select-arrow-wrapper .mat-select-arrow {
color: var(--on-surface);
margin-right: 50px;
}
.mat-select-value {
......@@ -224,3 +227,95 @@ Suggestion classes from Languagetool
}
}
}
/*
For quill
*/
::ng-deep .ql-editor.ql-blank::before {
color: var(--on-surface);
filter: opacity(0.6);
}
::ng-deep .ql-snow {
:focus {
outline-offset: 0;
}
.ql-stroke {
stroke: var(--on-surface);
}
.ql-picker {
color: var(--on-surface);
}
.ql-tooltip {
color: var(--on-surface);
background-color: var(--surface);
.ql-action::after {
padding: 7px !important;
background: var(--primary);
border-radius: 4px;
color: var(--on-primary);
}
.ql-remove::before {
padding: 7px !important;
background: var(--cancel);
border-radius: 4px;
color: var(--on-cancel);
}
&.ql-editing input[type=text] {
border-color: var(--on-surface);
color: var(--on-surface);
background-color: var(--dialog);
}
}
.ql-fill, .ql-stroke.ql-fill {
fill: var(--on-surface);
}
.ql-picker.ql-expanded .ql-picker-label {
color: var(--primary);
.ql-stroke {
stroke: var(--primary);
}
}
&.ql-container {
border-color: var(--on-surface);
}
&.ql-toolbar, .ql-toolbar {
border-color: var(--on-surface);
.ql-picker.ql-expanded {
.ql-picker-label {
border-color: var(--on-surface);
}
.ql-picker-options {
background-color: var(--surface);
}
}
button:hover, button:focus, button.ql-active,
.ql-picker-label:hover, .ql-picker-label.ql-active,
.ql-picker-item:hover, .ql-picker-item.ql-selected {
color: var(--primary);
.ql-stroke {
stroke: var(--primary);
}
.ql-fill, .ql-stroke.ql-fill {
fill: var(--primary);
}
}
}
}
import { AfterViewInit, Component, ElementRef, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { GrammarChecker } from '../../../utils/grammar-checker';
import { TranslateService } from '@ngx-translate/core';
import { LanguagetoolService } from '../../../services/http/languagetool.service';
import { Language, LanguagetoolResult, LanguagetoolService } from '../../../services/http/languagetool.service';
import { Comment } from '../../../models/comment';
import { User } from '../../../models/user';
import { NotificationService } from '../../../services/util/notification.service';
import { EventService } from '../../../services/util/event.service';
import { QuillEditorComponent } from 'ngx-quill';
import { CreateCommentKeywords } from '../../../utils/create-comment-keywords';
interface Mark {
range: Range;
marks: HTMLElement[];
}
@Component({
selector: 'app-write-comment',
......@@ -15,7 +21,8 @@ import { EventService } from '../../../services/util/event.service';
export class WriteCommentComponent implements OnInit, AfterViewInit {
@ViewChild('langSelect') langSelect: ElementRef<HTMLDivElement>;
@ViewChild('commentBody') commentBody: ElementRef<HTMLDivElement>;
@ViewChild('editor') editor: QuillEditorComponent;
@ViewChild('editorErrorLayer') editorErrorLayer: ElementRef<HTMLDivElement>;
@Input() user: User;
@Input() tags: string[];
@Input() onClose: () => any;
......@@ -28,14 +35,28 @@ export class WriteCommentComponent implements OnInit, AfterViewInit {
@Input() enabled = true;
comment: Comment;
selectedTag: string;
grammarChecker: GrammarChecker;
tempEditView: string;
currentHTML = '';
currentText = '';
//Grammarheck
languages: Language[] = ['de-DE', 'en-US', 'fr', 'auto'];
selectedLang: Language = 'auto';
isSpellchecking = false;
hasSpellcheckConfidence = true;
newLang = 'auto';
//Marks
currentMarks: Mark[] = [];
constructor(private notification: NotificationService,
private translateService: TranslateService,
public eventService: EventService,
public languagetoolService: LanguagetoolService) {
this.grammarChecker = new GrammarChecker(this.languagetoolService);
}
private static calcNodeTextSize(node: Node): number {
if (node instanceof HTMLBRElement) {
return 1;
}
return node.textContent.length;
}
ngOnInit(): void {
......@@ -43,7 +64,23 @@ export class WriteCommentComponent implements OnInit, AfterViewInit {
}
ngAfterViewInit() {
this.grammarChecker.initBehavior(() => this.commentBody.nativeElement, () => this.langSelect.nativeElement);
this.editor.onContentChanged.subscribe(e => {
this.currentHTML = e.html || '';
this.currentText = e.text;
});
this.editor.onEditorCreated.subscribe(_ => {
this.syncErrorLayer();
setTimeout(() => this.syncErrorLayer(), 200); // animations?
});
this.editor.onEditorChanged.subscribe(_ => this.syncErrorLayer());
}
clearHTML(): void {
this.editor.editorElem.innerHTML = '';
}
setHTML(html: string): void {
this.editor.editorElem.innerHTML = html;
}
buildCloseDialogActionCallback(): () => void {
......@@ -58,14 +95,227 @@ export class WriteCommentComponent implements OnInit, AfterViewInit {
return undefined;
}
return () => {
if (this.checkInputData(this.commentBody.nativeElement.innerText)) {
this.onSubmit(this.commentBody.nativeElement.innerText, this.selectedTag);
if (this.checkInputData(this.currentHTML)) {
this.onSubmit(this.currentHTML, this.selectedTag);
}
};
}
onTabChange() {
this.tempEditView = this.commentBody.nativeElement.innerText;
syncErrorLayer(): void {
const pos = this.editor.elementRef.nativeElement.getBoundingClientRect();
const elem = this.editorErrorLayer.nativeElement;
elem.style.width = pos.width + 'px';
elem.style.height = pos.height + 'px';
elem.style.marginBottom = '-' + elem.style.height;
}
onDocumentClick(e) {
const container = document.getElementsByClassName('dropdownBlock');
Array.prototype.forEach.call(container, (elem) => {
const hasMarkup = (e.target as Node).parentElement ? (e.target as Node).parentElement.classList.contains('markUp') : false;
if (!elem.contains(e.target) && (!hasMarkup ||
(e.target as HTMLElement).dataset.id !== (elem as Node).parentElement.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 text = e.clipboardData.getData('text');
const selection = window.getSelection();
const min = Math.min(selection.anchorOffset, selection.focusOffset);
const max = Math.max(selection.anchorOffset, selection.focusOffset);
const content = selection.anchorNode.textContent;
selection.anchorNode.textContent = content.substring(0, min) + text + content.substr(max);
const range = document.createRange();
const elem = selection.anchorNode instanceof HTMLElement ? selection.anchorNode.lastChild : selection.anchorNode;
range.setStart(elem, min + text.length);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
grammarCheck(rawText: string, langSelect: HTMLSpanElement): void {
this.onDocumentClick({
target: document
});
this.isSpellchecking = true;
this.hasSpellcheckConfidence = true;
const text = CreateCommentKeywords.cleaningFunction(rawText, true);
this.checkSpellings(text).subscribe((wordsCheck) => {
console.log(1);
if (!this.checkLanguageConfidence(wordsCheck)) {
this.hasSpellcheckConfidence = false;
this.isSpellchecking = false;
return;
}
// Hallo ich bin eine Entee. Ich liebe Gäse.
if (this.selectedLang === 'auto' &&
(langSelect.innerText.includes(this.newLang) || 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;
}
langSelect.innerHTML = this.newLang;
}
this.buildMarks(rawText, wordsCheck.matches.map(err => text.slice(err.offset, err.offset + err.length)));
}, () => {
this.isSpellchecking = false;
}, () => {
this.isSpellchecking = false;
});
}
buildMarks(initialText, wrongWords) {
const errorDiv = this.editorErrorLayer.nativeElement;
while (errorDiv.firstElementChild) {
errorDiv.firstElementChild.remove();
}
this.currentMarks.length = 0;
if (!wrongWords.length) {
return;
}
let currentElement: Node = this.editor.editorElem.firstElementChild;
let currentOffset = 0;
let depth = 0;
this.checkSpellings(initialText).subscribe((res) => {
for (const match of res.matches) {
const foundWord = initialText.slice(match.offset, match.offset + match.length);
if (!wrongWords.includes(foundWord)) {
continue;
}
let mark;
[currentElement, currentOffset, depth, mark] = this.createMarkAndRange(depth, currentElement,
currentOffset, match.offset, match.offset + match.length);
this.currentMarks.push(mark);
}
});
}
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);
}
private createMarkAndRange(depth: number,
currentElement: Node,
currentOffset: number,
start: number,
end: number): [Node, number, number, Mark] {
const range = document.createRange();
const marks: HTMLElement[] = [];
[currentElement, currentOffset, depth] = this.findNode(depth, currentElement, currentOffset, start);
range.setStart(currentElement, start - currentOffset);
[currentElement, currentOffset, depth] = this.findNode(depth, currentElement, currentOffset, end,
(node: Node) => {
//TODO Construct marks
});
range.setEnd(currentElement, end - currentOffset);
return [currentElement, currentOffset, depth, { range, marks }];
}
private findNode(depth: number,
currentElement: Node,
currentOffset: number,
target: number,
onNodeLeave?: (Node) => void): [Node, number, number] {
while (currentElement.firstChild) {
currentElement = currentElement.firstChild;
depth += 1;
}
let length = WriteCommentComponent.calcNodeTextSize(currentElement);
while (currentOffset + length <= target) {
if (onNodeLeave) {
onNodeLeave(currentElement);
}
currentOffset += length;
const wasAlreadyBreak = currentElement instanceof HTMLBRElement;
let currentBefore = currentElement.parentElement;
currentElement = currentElement.nextSibling;
while (!currentElement) {
if (depth === 0) {
throw new Error('The requested text position was not inside the container.');
}
if (depth === 1 && !wasAlreadyBreak) {
currentOffset += 1;
}
currentElement = currentBefore.nextSibling;
currentBefore = currentBefore.parentElement;
}
while (currentElement.firstChild) {
currentElement = currentElement.firstChild;
depth += 1;
}
length = WriteCommentComponent.calcNodeTextSize(currentElement);
}
return [currentElement, currentOffset, depth];
}
private createSuggestionHTML(commentBody: HTMLDivElement, result: LanguagetoolResult, index: number, wrongWord: string) {
const markUpDiv = document.createElement('div');
markUpDiv.classList.add('markUp');
markUpDiv.dataset.id = String(index);
const wordMarker = document.createElement('span');
wordMarker.dataset.id = String(index);
wordMarker.append(wrongWord);
markUpDiv.append(wordMarker);
const dropDownDiv = document.createElement('div');
dropDownDiv.classList.add('dropdownBlock');
markUpDiv.append(dropDownDiv);
markUpDiv.addEventListener('click', () => {
dropDownDiv.style.display = 'block';
const rectdiv = commentBody.getBoundingClientRect();
const rectmarkup = markUpDiv.getBoundingClientRect();
let offset;
if (rectmarkup.x + rectmarkup.width / 2 > rectdiv.right - 80) {
offset = rectdiv.right - rectmarkup.x - rectmarkup.width;
dropDownDiv.style.right = -offset + 'px';
} else if (rectmarkup.x + rectmarkup.width / 2 < rectdiv.left + 80) {
offset = rectmarkup.x - rectdiv.left;
dropDownDiv.style.left = -offset + 'px';
} else {
dropDownDiv.style.left = '50%';
dropDownDiv.style.marginLeft = '-80px';
}
});
const suggestions = result.matches[index].replacements;
if (!suggestions.length) {
const elem = document.createElement('span');
elem.classList.add('error-message');
elem.append(result.matches[index].message);
dropDownDiv.append(elem);
} else {
const length = suggestions.length > 3 ? 3 : suggestions.length;
for (let j = 0; j < length; j++) {
const elem = document.createElement('span');
elem.classList.add('suggestions');
elem.append(suggestions[j].value);
elem.addEventListener('click', () => {
elem.parentElement.parentElement.outerHTML = suggestions[j].value;
});
dropDownDiv.append(elem);
}
}
return markUpDiv;
}
private checkInputData(body: string): boolean {
......
import { Language, LanguagetoolResult, 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';
private commentBody: () => HTMLDivElement;
private langSelect: () => HTMLSpanElement;
constructor(private languagetoolService: LanguagetoolService) {
}
initBehavior(commentBody: () => HTMLDivElement, langSelect: () => HTMLSpanElement) {
this.commentBody = commentBody;
this.langSelect = langSelect;
}
onDocumentClick(e) {
const container = document.getElementsByClassName('dropdownBlock');
Array.prototype.forEach.call(container, (elem) => {
const hasMarkup = (e.target as Node).parentElement ? (e.target as Node).parentElement.classList.contains('markUp') : false;
if (!elem.contains(e.target) && (!hasMarkup ||
(e.target as HTMLElement).dataset.id !== (elem as Node).parentElement.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 text = e.clipboardData.getData('text');
const selection = window.getSelection();
const min = Math.min(selection.anchorOffset, selection.focusOffset);
const max = Math.max(selection.anchorOffset, selection.focusOffset);
const content = selection.anchorNode.textContent;
selection.anchorNode.textContent = content.substring(0, min) + text + content.substr(max);
const range = document.createRange();
const elem = selection.anchorNode instanceof HTMLElement ? selection.anchorNode.lastChild : selection.anchorNode;
range.setStart(elem, min + text.length);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
grammarCheck(commentBody: HTMLDivElement): void {
this.onDocumentClick({
target: document
});
const wrongWords: string[] = [];
this.isSpellchecking = true;
this.hasSpellcheckConfidence = true;
const unfilteredText = commentBody.innerText;
const text = CreateCommentKeywords.cleaningFunction(commentBody.innerText, true);
this.checkSpellings(text).subscribe((wordsCheck) => {
if (!this.checkLanguageConfidence(wordsCheck)) {
this.hasSpellcheckConfidence = false;
return;
}
if (this.selectedLang === 'auto' && (this.langSelect().innerText.includes(this.newLang)
|| this.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;
}
this.langSelect().innerHTML = this.newLang;
}
if (wordsCheck.matches.length <= 0) {
return;
}
wordsCheck.matches.forEach(grammarError => {
const wrongWord = text.slice(grammarError.offset, grammarError.offset + grammarError.length);
wrongWords.push(wrongWord);
});
let lastFound = unfilteredText.length;
this.checkSpellings(unfilteredText).subscribe((res) => {
commentBody.innerHTML = '';
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 = unfilteredText.slice(start, end);
if (!wrongWords.includes(wrongWord)) {
continue;
}
if (lastFound > end) {
commentBody.prepend(unfilteredText.slice(end, lastFound));
}
commentBody.prepend(this.createSuggestionHTML(res, i, wrongWord));
lastFound = res.matches[i].offset;
}
if (lastFound > 0) {
commentBody.prepend(unfilteredText.slice(0, lastFound));
}
});
}, () => {
this.isSpellchecking = false;
}, () => {
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);
}
private createSuggestionHTML(result: LanguagetoolResult, index: number, wrongWord: string) {
const markUpDiv = document.createElement('div');
markUpDiv.classList.add('markUp');
markUpDiv.dataset.id = String(index);
const wordMarker = document.createElement('span');
wordMarker.dataset.id = String(index);
wordMarker.append(wrongWord);
markUpDiv.append(wordMarker);
const dropDownDiv = document.createElement('div');
dropDownDiv.classList.add('dropdownBlock');
markUpDiv.append(dropDownDiv);
markUpDiv.addEventListener('click', () => {
dropDownDiv.style.display = 'block';
const rectdiv = this.commentBody().getBoundingClientRect();
const rectmarkup = markUpDiv.getBoundingClientRect();
let offset;
if (rectmarkup.x + rectmarkup.width / 2 > rectdiv.right - 80) {
offset = rectdiv.right - rectmarkup.x - rectmarkup.width;
dropDownDiv.style.right = -offset + 'px';
} else if (rectmarkup.x + rectmarkup.width / 2 < rectdiv.left + 80) {
offset = rectmarkup.x - rectdiv.left;
dropDownDiv.style.left = -offset + 'px';
} else {
dropDownDiv.style.left = '50%';
dropDownDiv.style.marginLeft = '-80px';
}
});
const suggestions = result.matches[index].replacements;
if (!suggestions.length) {
const elem = document.createElement('span');
elem.classList.add('error-message');
elem.append(result.matches[index].message);
dropDownDiv.append(elem);
} else {
const length = suggestions.length > 3 ? 3 : suggestions.length;
for (let j = 0; j < length; j++) {
const elem = document.createElement('span');
elem.classList.add('suggestions');
elem.append(suggestions[j].value);
elem.addEventListener('click', () => {
elem.parentElement.parentElement.outerHTML = suggestions[j].value;
});
dropDownDiv.append(elem);
}
}
return markUpDiv;
}
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment