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 8d133d0083abd17604e4bf9a2321dae7c941ea58..64440f23d58d80dd97437a359a57f9327186229a 100644 --- a/src/app/components/shared/comment-answer/comment-answer.component.html +++ b/src/app/components/shared/comment-answer/comment-answer.component.html @@ -13,7 +13,7 @@ <app-write-comment [user]="user" [onClose]="openDeleteAnswerDialog()" [onSubmit]="saveAnswer()" - [disableCancelButton]="!answer && commentComponent && commentComponent.commentBody && !commentComponent.commentBody.nativeElement.innerText" + [disableCancelButton]="!answer && commentComponent && commentComponent.commentData.currentText.length > 0" [confirmLabel]="'save-answer'" [cancelLabel]="'delete-answer'" [additionalTemplate]="editAnswer" @@ -25,7 +25,7 @@ <ng-template #editAnswer> <div *ngIf="(isStudent || !edit) && answer"> - <app-custom-markdown class="imborder-answerages" [data]="answer"></app-custom-markdown> + <app-view-comment-data [currentData]="answer"></app-view-comment-data> <div fxLayout="row" fxLayoutAlign="end"> <button mat-raised-button 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 c83a515698c36e09a325e13ef206fd558fe64d89..7a4174f7656ff0693dcaf6a9b7135859fc67dbea 100644 --- a/src/app/components/shared/comment-answer/comment-answer.component.ts +++ b/src/app/components/shared/comment-answer/comment-answer.component.ts @@ -84,7 +84,7 @@ export class CommentAnswerComponent implements OnInit { } deleteAnswer() { - this.commentComponent.clearHTML(); + this.commentComponent.commentData.clear(); this.answer = null; this.commentService.answer(this.comment, this.answer).subscribe(); this.translateService.get('comment-page.answer-deleted').subscribe(msg => { @@ -94,6 +94,6 @@ export class CommentAnswerComponent implements OnInit { onEditClick() { this.edit = true; - setTimeout(() => this.commentComponent.setHTML(this.answer)); + setTimeout(() => this.commentComponent.commentData.set(this.answer)); } } diff --git a/src/app/components/shared/comment/comment.component.html b/src/app/components/shared/comment/comment.component.html index bd009b1e970a3438c0d77feb16a6287ee4112155..0bd1627a2cb29871b30e05f34c6bfe9471fd0c49 100644 --- a/src/app/components/shared/comment/comment.component.html +++ b/src/app/components/shared/comment/comment.component.html @@ -264,7 +264,7 @@ tabindex="0"> <ars-row #commentBody> <ars-row #commentBodyInner> - <app-custom-markdown class="images" [data]="comment.body"></app-custom-markdown> + <app-view-comment-data class="images" [currentData]="comment.body"></app-view-comment-data> </ars-row> </ars-row> <span id="comment-{{ comment.id }}" diff --git a/src/app/components/shared/custom-markdown/custom-markdown.component.ts b/src/app/components/shared/custom-markdown/custom-markdown.component.ts index 7e7fb10142840e8bd61cf86fdf67c6365e66cc25..9b00e52d5a73d8c688ad723a29a30399cd68032e 100644 --- a/src/app/components/shared/custom-markdown/custom-markdown.component.ts +++ b/src/app/components/shared/custom-markdown/custom-markdown.component.ts @@ -14,7 +14,6 @@ export class CustomMarkdownComponent implements OnChanges { @Input() start: number | undefined; @Input() line: string | string[] | undefined; @Input() lineOffset: number | undefined; - @Input() isRawHTML = false; @Input() katexOptions: KatexOptions = { throwOnError: true }; @@ -89,10 +88,6 @@ export class CustomMarkdownComponent implements OnChanges { } private render(markdown: string, decodeHtml = false): void { - if (this.isRawHTML) { - this.element.nativeElement.innerHTML = this.renderKatex(markdown); - return; - } if (this.katex) { markdown = CustomMarkdownComponent.fixKatex(markdown); } diff --git a/src/app/components/shared/shared.module.ts b/src/app/components/shared/shared.module.ts index 3e435fad18f0ab647cd9aedfd366d481385c22a0..52a78cd959336e7bc6839c700a1bf873a3ec0c75 100644 --- a/src/app/components/shared/shared.module.ts +++ b/src/app/components/shared/shared.module.ts @@ -50,6 +50,7 @@ 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'; +import { ViewCommentDataComponent } from './view-comment-data/view-comment-data.component'; @NgModule({ imports: [ @@ -105,7 +106,8 @@ import { QuillModule } from 'ngx-quill'; MatSpinnerOverlayComponent, WriteCommentComponent, CustomMarkdownComponent, - ScrollIntoViewDirective + ScrollIntoViewDirective, + ViewCommentDataComponent ], exports: [ RoomJoinComponent, diff --git a/src/app/components/shared/view-comment-data/view-comment-data.component.html b/src/app/components/shared/view-comment-data/view-comment-data.component.html new file mode 100644 index 0000000000000000000000000000000000000000..dbad03832ad80e76b87f56cbe672507c253b81e0 --- /dev/null +++ b/src/app/components/shared/view-comment-data/view-comment-data.component.html @@ -0,0 +1,22 @@ +<ars-row *ngIf="isEditor"> + <div #editorErrorLayer id="editorErrorLayer"></div> + <quill-editor #editor + [maxLength]="user.role === 3 ? 1000 : 500" + placeholder="{{ 'comment-page.enter-comment' | translate }}" + [modules]="quillModules" + (document:click)="onDocumentClick($event)"> + </quill-editor> + <div #tooltipContainer></div> + <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%"> + {{currentData.length}} / {{user.role === 3 ? 1000 : 500}} + </span> + </div> +</ars-row> +<div *ngIf="!isEditor"> + <quill-view #quillView [modules]="quillModules"> + </quill-view> +</div> diff --git a/src/app/components/shared/view-comment-data/view-comment-data.component.scss b/src/app/components/shared/view-comment-data/view-comment-data.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..ff0d11f04b75d32fa8af760424ba22d0856a4b07 --- /dev/null +++ b/src/app/components/shared/view-comment-data/view-comment-data.component.scss @@ -0,0 +1,114 @@ +::ng-deep .ql-editor.ql-blank::before { + color: var(--on-surface); + filter: opacity(0.6); +} + +::ng-deep .ql-snow { + :focus { + outline-offset: 0; + } + + .ql-editor { + min-height: 8.8em; + } + + .ql-stroke { + stroke: var(--on-surface); + } + + .ql-picker { + color: var(--on-surface); + } + + .ql-tooltip { + &[data-mode=formula]::before { + --quill-tooltip-label: var(--quill-tooltip-label-formula); + } + &[data-mode=video]::before { + --quill-tooltip-label: var(--quill-tooltip-label-video); + } + &[data-mode=image]::before { + --quill-tooltip-label: var(--quill-tooltip-label-image); + } + &[data-mode=link]::before { + --quill-tooltip-label: var(--quill-tooltip-label-link); + } + &::before { + content: var(--quill-tooltip-label) !important; + } + + color: var(--on-surface); + background-color: var(--surface); + + .ql-action::after { + padding: 7px !important; + background: var(--primary); + border-radius: 4px; + color: var(--on-primary); + content: var(--quill-tooltip-action) !important; + } + + &.ql-editing .ql-action::after { + --quill-tooltip-action: var(--quill-tooltip-action-save); + } + + .ql-remove::before { + content: var(--quill-tooltip-remove) !important; + 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); + height: 80%; + } + + &.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); + } + } + } +} diff --git a/src/app/components/shared/view-comment-data/view-comment-data.component.spec.ts b/src/app/components/shared/view-comment-data/view-comment-data.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a2b65bc5abe829284a47c2feb6094a6929b74d0 --- /dev/null +++ b/src/app/components/shared/view-comment-data/view-comment-data.component.spec.ts @@ -0,0 +1,26 @@ +/*import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ViewCommentDataComponent } from './view-comment-data.component'; + +describe('ViewCommentDataComponent', () => { + let component: ViewCommentDataComponent; + let fixture: ComponentFixture<ViewCommentDataComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ViewCommentDataComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ViewCommentDataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); + */ diff --git a/src/app/components/shared/view-comment-data/view-comment-data.component.ts b/src/app/components/shared/view-comment-data/view-comment-data.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..06d4e031d584dc0bc2b4720d7c46547f59183408 --- /dev/null +++ b/src/app/components/shared/view-comment-data/view-comment-data.component.ts @@ -0,0 +1,245 @@ +import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { User } from '../../../models/user'; +import { QuillEditorComponent, QuillModules, QuillViewComponent } from 'ngx-quill'; +import Delta from 'quill-delta'; +import Quill from 'quill'; +import ImageResize from 'quill-image-resize-module'; +import 'quill-emoji/dist/quill-emoji.js'; +import { LanguageService } from '../../../services/util/language.service'; +import { TranslateService } from '@ngx-translate/core'; + +Quill.register('modules/imageResize', ImageResize); + +const participantToolbar = [ + ['bold', 'strike'], + ['blockquote', 'code-block'], + [{ list: 'ordered' }, { list: 'bullet' }], + ['link', 'formula'], + ['emoji'] +]; + +const moderatorToolbar = [ + ['bold', 'strike'], + ['blockquote', 'code-block'], + [{ header: 1 }, { header: 2 }], + [{ list: 'ordered' }, { list: 'bullet' }], + [{ indent: '-1' }, { indent: '+1' }], + [{ color: [] }], + [{ align: [] }], + ['link', 'image', 'video', 'formula'], + ['emoji'] +]; + +@Component({ + selector: 'app-view-comment-data', + templateUrl: './view-comment-data.component.html', + styleUrls: ['./view-comment-data.component.scss'] +}) +export class ViewCommentDataComponent implements OnInit, AfterViewInit { + + @ViewChild('editor') editor: QuillEditorComponent; + @ViewChild('quillView') quillView: QuillViewComponent; + @ViewChild('editorErrorLayer') editorErrorLayer: ElementRef<HTMLDivElement>; + @ViewChild('tooltipContainer') tooltipContainer: ElementRef<HTMLDivElement>; + @Input() isEditor = false; + @Input() user: User; + @Input() currentData = ''; + @Input() markEvents?: { + onCreate: (markContainer: HTMLDivElement, tooltipContainer: HTMLDivElement, editor: QuillEditorComponent) => void; + onChange: (delta: any) => void; + onEditorChange: () => void; + onDocumentClick: (e) => void; + }; + currentText = ''; + + quillModules: QuillModules = { + toolbar: { + container: participantToolbar, + handlers: { + image: () => this.handle('image'), + video: () => this.handle('video'), + link: () => this.handleLink(), + formula: () => this.handle('formula') + } + }, + 'emoji-toolbar': true, + 'emoji-shortname': true, + imageResize: { + modules: ['Resize', 'DisplaySize'] + } + }; + + constructor(private languageService: LanguageService, + private translateService: TranslateService) { + this.languageService.langEmitter.subscribe(lang => { + this.translateService.use(lang); + if (this.isEditor) { + this.updateCSSVariables(); + } + }); + } + + private static getDataFromDelta(contentDelta) { + return JSON.stringify(contentDelta.ops.map(op => { + let hasOnlyInsert = true; + for (const key in op) { + if (key !== 'insert') { + hasOnlyInsert = false; + break; + } + } + return hasOnlyInsert ? op['insert'] : op; + })); + } + + private static getDeltaFromData(jsonData: string) { + return { + ops: JSON.parse(jsonData).map(elem => { + if (!elem['insert']) { + return { insert: elem }; + } else { + return elem; + } + }) + }; + } + + ngOnInit(): void { + if (this.user && this.user.role > 0) { + this.quillModules.toolbar['container'] = moderatorToolbar; + } + this.translateService.use(localStorage.getItem('currentLang')); + if (this.isEditor) { + this.updateCSSVariables(); + } + } + + ngAfterViewInit() { + if (this.isEditor) { + this.editor.onContentChanged.subscribe(e => { + if (this.markEvents && this.markEvents.onChange) { + this.markEvents.onChange(e.delta); + } + this.currentData = ViewCommentDataComponent.getDataFromDelta(e.content); + this.currentText = e.text; + }); + this.editor.onEditorCreated.subscribe(_ => { + if (this.markEvents && this.markEvents.onCreate) { + this.markEvents.onCreate(this.editorErrorLayer.nativeElement, this.tooltipContainer.nativeElement, this.editor); + } + this.syncErrorLayer(); + setTimeout(() => this.syncErrorLayer(), 200); // animations? + }); + this.editor.onEditorChanged.subscribe(_ => { + if (this.markEvents && this.markEvents.onEditorChange) { + this.markEvents.onEditorChange(); + } + this.syncErrorLayer(); + }); + } else { + this.quillView.onEditorCreated.subscribe(_ => { + this.set(this.currentData); + }); + } + } + + onDocumentClick(e) { + if (this.markEvents && this.markEvents.onDocumentClick) { + this.markEvents.onDocumentClick(e); + } + } + + clear(): void { + const delta = new Delta(); + if (this.isEditor) { + this.editor.quillEditor.setContents(delta); + } else { + this.quillView.quillEditor.setContents(delta); + } + } + + set(jsonData: string): void { + const delta = ViewCommentDataComponent.getDeltaFromData(jsonData); + if (this.isEditor) { + this.editor.quillEditor.setContents(delta); + } else { + this.quillView.quillEditor.setContents(delta); + } + } + + private 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; + } + + private handleLink(): void { + const quill = this.editor.quillEditor; + const selection = quill.getSelection(false); + if (!selection || !selection.length) { + return; + } + const tooltip = quill.theme.tooltip; + const originalSave = tooltip.save; + const originalHide = tooltip.hide; + tooltip.save = () => { + const value = tooltip.textbox.value; + if (value) { + const delta = new Delta() + .retain(selection.index) + .retain(selection.length, { link: value }); + quill.updateContents(delta); + tooltip.hide(); + } + }; + // Called on hide and save. + tooltip.hide = () => { + tooltip.save = originalSave; + tooltip.hide = originalHide; + tooltip.hide(); + }; + tooltip.edit('link'); + tooltip.textbox.value = quill.getText(selection.index, selection.length); + this.translateService.get('quill.tooltip-placeholder-link') + .subscribe(translation => tooltip.textbox.placeholder = translation); + } + + private handle(type: string): void { + const quill = this.editor.quillEditor; + const tooltip = quill.theme.tooltip; + const originalSave = tooltip.save; + const originalHide = tooltip.hide; + tooltip.save = () => { + const range = quill.getSelection(true); + const value = tooltip.textbox.value; + if (value) { + quill.insertEmbed(range.index, type, value, 'user'); + } + }; + // Called on hide and save. + tooltip.hide = () => { + tooltip.save = originalSave; + tooltip.hide = originalHide; + tooltip.hide(); + }; + tooltip.edit(type); + this.translateService.get('quill.tooltip-placeholder-' + type) + .subscribe(translation => tooltip.textbox.placeholder = translation); + } + + private updateCSSVariables() { + const variables = [ + 'quill.tooltip-remove', 'quill.tooltip-action-save', 'quill.tooltip-action', 'quill.tooltip-label', + 'quill.tooltip-label-link', 'quill.tooltip-label-image', 'quill.tooltip-label-video', + 'quill.tooltip-label-formula' + ]; + for (const variable of variables) { + this.translateService.get(variable).subscribe(translation => { + document.body.style.setProperty('--' + variable.replace('.', '-'), JSON.stringify(translation)); + }); + } + } + +} diff --git a/src/app/components/shared/write-comment/write-comment.component.html b/src/app/components/shared/write-comment/write-comment.component.html index b931a1daaf5a5cafb7d813d73e1a848bc72312c5..1b180e34a6c5b10e3dd707d588b2d1aa90ff1b13 100644 --- a/src/app/components/shared/write-comment/write-comment.component.html +++ b/src/app/components/shared/write-comment/write-comment.component.html @@ -45,31 +45,17 @@ </div> </ars-row> <ars-row [height]="12"></ars-row> -<ars-row *ngIf="enabled"> - <div #editorErrorLayer id="editorErrorLayer"></div> - <quill-editor #editor - [maxLength]="user.role === 3 ? 1000 : 500" - placeholder="{{ 'comment-page.enter-comment' | translate }}" - [modules]="quillModules" - (document:click)="onDocumentClick($event)"> - </quill-editor> - <div #tooltipContainer></div> - <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> +<app-view-comment-data *ngIf="enabled" + [user]="user" + [isEditor]="true" + [markEvents]="getMarkEvents()"></app-view-comment-data> <ars-row ars-flex-box *ngIf="enabled" class="spellcheck"> <ars-col> <button - [disabled]="currentText.length < 4" + [disabled]="!commentData || commentData.currentText.length < 4" mat-flat-button class="spell-button" - (click)="grammarCheck(currentText, langSelect && langSelect.nativeElement)"> + (click)="checkGrammar()"> {{ 'comment-page.grammar-check' | translate}} <mat-icon *ngIf="isSpellchecking" class="spinner-container"> <app-mat-spinner-overlay diameter="20" strokeWidth="2" [color]="'on-primary'"></app-mat-spinner-overlay> diff --git a/src/app/components/shared/write-comment/write-comment.component.scss b/src/app/components/shared/write-comment/write-comment.component.scss index 2579d849f5dc293cd28563a01bd3209d250dda7c..0f303ef0146cb8af2965e175a00a2eadd31dd795 100644 --- a/src/app/components/shared/write-comment/write-comment.component.scss +++ b/src/app/components/shared/write-comment/write-comment.component.scss @@ -249,6 +249,10 @@ $borderOffset: 2px; outline-offset: 0; } + .ql-editor { + min-height: 8.8em; + } + .ql-stroke { stroke: var(--on-surface); } diff --git a/src/app/components/shared/write-comment/write-comment.component.ts b/src/app/components/shared/write-comment/write-comment.component.ts index 1d4d08f4e13679ec0f3e251d5d91031a93a0e008..10352624d695b58fa7762d5961ca21fb2e9fbc10 100644 --- a/src/app/components/shared/write-comment/write-comment.component.ts +++ b/src/app/components/shared/write-comment/write-comment.component.ts @@ -5,46 +5,22 @@ 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, QuillModules } from 'ngx-quill'; import { CreateCommentKeywords } from '../../../utils/create-comment-keywords'; import { Marks } from './write-comment.marks'; import { LanguageService } from '../../../services/util/language.service'; -import Delta from 'quill-delta'; -import Quill from 'quill'; -import ImageResize from 'quill-image-resize-module'; -import 'quill-emoji/dist/quill-emoji.js'; +import { QuillEditorComponent } from 'ngx-quill'; +import { ViewCommentDataComponent } from '../view-comment-data/view-comment-data.component'; -Quill.register('modules/imageResize', ImageResize); - -const participantToolbar = [ - ['bold', 'strike'], - ['blockquote', 'code-block'], - [{ list: 'ordered' }, { list: 'bullet' }], - ['link', 'formula'] -]; - -const moderatorToolbar = [ - ['bold', 'strike'], - ['blockquote', 'code-block'], - [{ header: 1 }, { header: 2 }], - [{ list: 'ordered' }, { list: 'bullet' }], - [{ indent: '-1' }, { indent: '+1' }], - [{ color: [] }], - [{ align: [] }], - ['link', 'image', 'video', 'formula'] -]; @Component({ selector: 'app-write-comment', templateUrl: './write-comment.component.html', styleUrls: ['./write-comment.component.scss'] }) -export class WriteCommentComponent implements OnInit, AfterViewInit { +export class WriteCommentComponent implements OnInit { + @ViewChild(ViewCommentDataComponent) commentData: ViewCommentDataComponent; @ViewChild('langSelect') langSelect: ElementRef<HTMLDivElement>; - @ViewChild('editor') editor: QuillEditorComponent; - @ViewChild('editorErrorLayer') editorErrorLayer: ElementRef<HTMLDivElement>; - @ViewChild('tooltipContainer') tooltipContainer: ElementRef<HTMLDivElement>; @Input() user: User; @Input() tags: string[]; @Input() onClose: () => any; @@ -57,32 +33,14 @@ export class WriteCommentComponent implements OnInit, AfterViewInit { @Input() enabled = true; comment: Comment; selectedTag: string; - currentHTML = ''; - currentText = ''; - //Grammarheck + // Grammarheck languages: Language[] = ['de-DE', 'en-US', 'fr', 'auto']; selectedLang: Language = 'auto'; isSpellchecking = false; hasSpellcheckConfidence = true; newLang = 'auto'; + // Marks marks: Marks; - quillModules: QuillModules = { - toolbar: { - container: participantToolbar, - handlers: { - image: () => this.handle('image'), - video: () => this.handle('video'), - link: () => this.handleLink(), - formula: () => this.handle('formula') - } - }, - 'emoji-toolbar': true, - 'emoji-textarea': true, - 'emoji-shortname': true, - imageResize: { - modules: ['Resize', 'DisplaySize', 'Toolbar'] - } - }; constructor(private notification: NotificationService, private languageService: LanguageService, @@ -91,55 +49,11 @@ export class WriteCommentComponent implements OnInit, AfterViewInit { public languagetoolService: LanguagetoolService) { this.languageService.langEmitter.subscribe(lang => { this.translateService.use(lang); - this.updateCSSVariables(); }); } ngOnInit(): void { this.translateService.use(localStorage.getItem('currentLang')); - if (this.user && this.user.role > 0) { - this.quillModules.toolbar['container'] = moderatorToolbar; - } - this.translateService.use(localStorage.getItem('currentLang')); - this.updateCSSVariables(); - } - - ngAfterViewInit() { - this.editor.onContentChanged.subscribe(e => { - this.marks.onDataChange(e.delta); - this.currentHTML = e.html || ''; - this.currentText = e.text; - }); - this.editor.onEditorCreated.subscribe(_ => { - this.marks = new Marks(this.editorErrorLayer.nativeElement, this.tooltipContainer.nativeElement, this.editor); - this.syncErrorLayer(); - setTimeout(() => this.syncErrorLayer(), 200); // animations? - }); - this.editor.onEditorChanged.subscribe(_ => { - const elem: HTMLDivElement = document.querySelector('div.ql-tooltip'); - if (elem) { // fix tooltip - setTimeout(() => { - const left = parseFloat(elem.style.left); - const right = left + elem.getBoundingClientRect().width; - const containerWidth = this.editor.editorElem.getBoundingClientRect().width; - if (left < 0) { - elem.style.left = '0'; - } else if (right > containerWidth) { - elem.style.left = (containerWidth - right + left) + 'px'; - } - }); - } - this.syncErrorLayer(); - this.marks.sync(); - }); - } - - clearHTML(): void { - this.editor.editorElem.innerHTML = ''; - } - - setHTML(html: string): void { - this.editor.editorElem.innerHTML = html; } buildCloseDialogActionCallback(): () => void { @@ -154,36 +68,22 @@ export class WriteCommentComponent implements OnInit, AfterViewInit { return undefined; } return () => { - if (this.checkInputData(this.currentHTML)) { - this.onSubmit(this.currentHTML, this.selectedTag); + if (this.checkInputData(this.commentData.currentData)) { + this.onSubmit(this.commentData.currentData, this.selectedTag); } }; } - 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) { if (!this.marks) { return; } - const range = this.editor.quillEditor.getSelection(false); + const range = this.commentData.editor.quillEditor.getSelection(false); this.marks.onClick(range && range.length === 0 ? range.index : null); } - 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>', ''); - } + checkGrammar() { + this.grammarCheck(this.commentData.currentText, this.langSelect && this.langSelect.nativeElement); } grammarCheck(rawText: string, langSelect: HTMLSpanElement): void { @@ -233,6 +133,34 @@ export class WriteCommentComponent implements OnInit, AfterViewInit { return this.languagetoolService.checkSpellings(text, language); } + getMarkEvents() { + return { + onCreate: (markContainer: HTMLDivElement, tooltipContainer: HTMLDivElement, editor: QuillEditorComponent) => { + this.marks = new Marks(markContainer, tooltipContainer, editor); + }, + onChange: (delta: any) => { + this.marks.onDataChange(delta); + }, + onEditorChange: () => { + const elem: HTMLDivElement = document.querySelector('div.ql-tooltip'); + if (elem) { // fix tooltip + setTimeout(() => { + const left = parseFloat(elem.style.left); + const right = left + elem.getBoundingClientRect().width; + const containerWidth = this.commentData.editor.editorElem.getBoundingClientRect().width; + if (left < 0) { + elem.style.left = '0'; + } else if (right > containerWidth) { + elem.style.left = (containerWidth - right + left) + 'px'; + } + }); + } + this.marks.sync(); + }, + onDocumentClick: (e) => this.onDocumentClick(e) + }; + } + private checkInputData(body: string): boolean { body = body.trim(); if (!body) { @@ -244,71 +172,4 @@ export class WriteCommentComponent implements OnInit, AfterViewInit { return true; } - private updateCSSVariables() { - const variables = [ - 'quill.tooltip-remove', 'quill.tooltip-action-save', 'quill.tooltip-action', 'quill.tooltip-label', - 'quill.tooltip-label-link', 'quill.tooltip-label-image', 'quill.tooltip-label-video', - 'quill.tooltip-label-formula' - ]; - for (const variable of variables) { - this.translateService.get(variable).subscribe(translation => { - document.body.style.setProperty('--' + variable.replace('.', '-'), JSON.stringify(translation)); - }); - } - } - - private handleLink(): void { - const quill = this.editor.quillEditor; - const selection = quill.getSelection(false); - if (!selection || !selection.length) { - return; - } - const tooltip = quill.theme.tooltip; - const originalSave = tooltip.save; - const originalHide = tooltip.hide; - tooltip.save = () => { - const value = tooltip.textbox.value; - if (value) { - const delta = new Delta() - .retain(selection.index) - .retain(selection.length, { link: value }); - quill.updateContents(delta); - tooltip.hide(); - } - }; - // Called on hide and save. - tooltip.hide = () => { - tooltip.save = originalSave; - tooltip.hide = originalHide; - tooltip.hide(); - }; - tooltip.edit('link'); - tooltip.textbox.value = quill.getText(selection.index, selection.length); - this.translateService.get('quill.tooltip-placeholder-link') - .subscribe(translation => tooltip.textbox.placeholder = translation); - } - - private handle(type: string): void { - const quill = this.editor.quillEditor; - const tooltip = quill.theme.tooltip; - const originalSave = tooltip.save; - const originalHide = tooltip.hide; - tooltip.save = () => { - const range = quill.getSelection(true); - const value = tooltip.textbox.value; - if (value) { - quill.insertEmbed(range.index, type, value, 'user'); - } - }; - // Called on hide and save. - tooltip.hide = () => { - tooltip.save = originalSave; - tooltip.hide = originalHide; - tooltip.hide(); - }; - tooltip.edit(type); - this.translateService.get('quill.tooltip-placeholder-' + type) - .subscribe(translation => tooltip.textbox.placeholder = translation); - } - } diff --git a/src/app/components/shared/write-comment/write-comment.marks.ts b/src/app/components/shared/write-comment/write-comment.marks.ts index b387e18f949bd064eb99ca0ca1ba442870b4287c..d65fa2e88459694442ebd21cd1546dcda65b1b5f 100644 --- a/src/app/components/shared/write-comment/write-comment.marks.ts +++ b/src/app/components/shared/write-comment/write-comment.marks.ts @@ -1,6 +1,40 @@ import { LanguagetoolResult } from '../../../services/http/languagetool.service'; import { QuillEditorComponent } from 'ngx-quill'; +class ContentIndexFinder { + + private opIndex = 0; + private contentOffset = 0; + private textOffset = 0; + + constructor(private contentOps) { + } + + adjustTextIndexes(startIndex: number, length: number): [number, number] { + const endIndex = startIndex + length; + let textLen = typeof this.contentOps[this.opIndex]['insert'] === 'string' ? + this.contentOps[this.opIndex]['insert'].length : 0; + while (textLen === 0 || this.textOffset + textLen < startIndex) { + this.textOffset += textLen; + this.contentOffset += textLen === 0 ? 1 : textLen; + ++this.opIndex; + textLen = typeof this.contentOps[this.opIndex]['insert'] === 'string' ? + this.contentOps[this.opIndex]['insert'].length : 0; + } + const diff = this.contentOffset - this.textOffset; + startIndex += diff; + while (this.textOffset + textLen < endIndex) { + this.textOffset += textLen; + this.contentOffset += textLen === 0 ? 1 : textLen; + ++this.opIndex; + textLen = typeof this.contentOps[this.opIndex]['insert'] === 'string' ? + this.contentOps[this.opIndex]['insert'].length : 0; + } + length += this.contentOffset - this.textOffset - diff; + return [startIndex, length]; + } +} + export class Marks { private textErrors: Mark[] = []; @@ -31,13 +65,13 @@ export class Marks { let index = 0; for (const op of delta.ops) { if (op['insert']) { - const len = op['insert'].length; + const len = typeof op['insert'] === 'string' ? op['insert'].length : 1; for (const textError of this.textErrors) { if (index > textError.startIndex + textError.markLength) { continue; } - textError.markLength += len; if (index >= textError.startIndex) { + textError.markLength += len; continue; } textError.startIndex += len; @@ -62,7 +96,6 @@ export class Marks { } if (endDelete < textError.startIndex) { textError.startIndex -= len; - textError.markLength -= len; return true; } if (endDelete < textError.startIndex + textError.markLength) { @@ -82,13 +115,15 @@ export class Marks { } buildErrors(initialText: string, wrongWords: string[], res: LanguagetoolResult): void { + const indexFinder = new ContentIndexFinder(this.editor.quillEditor.getContents().ops); for (let i = 0; i < res.matches.length; i++) { const match = res.matches[i]; const foundWord = initialText.slice(match.offset, match.offset + match.length); if (!wrongWords.includes(foundWord)) { continue; } - const mark = new Mark(match.offset, match.length, this.markContainer, this.tooltipContainer, this.editor.quillEditor); + const [start, len] = indexFinder.adjustTextIndexes(match.offset, match.length); + const mark = new Mark(start, len, this.markContainer, this.tooltipContainer, this.editor.quillEditor); mark.setSuggestions(res, i, () => { const index = this.textErrors.findIndex(elem => elem === mark); if (index >= 0) { @@ -207,19 +242,33 @@ class Mark { } private calculateBoundaries(): [start: number, length: number][] { - const text: string = this.quillEditor.getText(this.startIndex, this.markLength); + const ops = this.quillEditor.getContents(this.startIndex, this.markLength).ops; const bounds = []; - let i = text.indexOf('\n'); let currentIndex = 0; - while (i >= 0) { - if (i > currentIndex) { - bounds.push([this.startIndex + currentIndex, i - currentIndex]); + for (const op of ops) { + if (typeof op['insert'] === 'string') { + const text = op['insert']; + let i = text.indexOf('\n'); + let findIndex = 0; + while (i >= 0) { + if (i > findIndex) { + bounds.push([this.startIndex + findIndex + currentIndex, i - findIndex]); + } + findIndex = i + 1; + i = text.indexOf('\n', findIndex); + } + if (text.length + currentIndex < this.markLength) { + if (text.length > findIndex) { + bounds.push([this.startIndex + findIndex + currentIndex, text.length - findIndex]); + } + } else if (this.markLength > findIndex + currentIndex && text.length > findIndex) { + bounds.push([this.startIndex + findIndex + currentIndex, this.markLength - findIndex - currentIndex]); + } + currentIndex += text.length; + } else { + bounds.push([this.startIndex + currentIndex, 1]); + ++currentIndex; } - currentIndex = i + 1; - i = text.indexOf('\n', currentIndex); - } - if (this.markLength > currentIndex) { - bounds.push([this.startIndex + currentIndex, this.markLength - currentIndex]); } return bounds; }