diff --git a/src/app/components/fragments/markdown-toolbar/markdown-toolbar.component.html b/src/app/components/fragments/markdown-toolbar/markdown-toolbar.component.html index e189bcac0ff0fd0f921e4963963c3d470447a787..41c2fd74574017bac57462d65f2a23417c5d91a5 100644 --- a/src/app/components/fragments/markdown-toolbar/markdown-toolbar.component.html +++ b/src/app/components/fragments/markdown-toolbar/markdown-toolbar.component.html @@ -1,4 +1,4 @@ -<div class="markdown-toolbar" *ngIf="textarea"> +<div class="markdown-toolbar"> <button mat-icon-button *ngFor="let button of buttons" (click)="onClick($event, button);"> <mat-icon attr.aria-label="{{button.label}}">{{button.icon}}</mat-icon> </button> diff --git a/src/app/components/fragments/markdown-toolbar/markdown-toolbar.component.ts b/src/app/components/fragments/markdown-toolbar/markdown-toolbar.component.ts index 37fb6e0f3dbff491137e180e352026ba34e387e6..22755da4d05a339df84c79630ff1acb82d4a6ef0 100644 --- a/src/app/components/fragments/markdown-toolbar/markdown-toolbar.component.ts +++ b/src/app/components/fragments/markdown-toolbar/markdown-toolbar.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, Input, OnDestroy } from '@angular/core'; +import { Component, Input } from '@angular/core'; @Component({ selector: 'app-markdown-toolbar', @@ -6,11 +6,10 @@ import { AfterViewInit, Component, Input, OnDestroy } from '@angular/core'; styleUrls: ['./markdown-toolbar.component.scss'] }) /** - * Component offering markdown action buttons for a textarea. Associated via input attribute containing the textare's HTML id - * First approach, to add the toolbar via directive directly to the textarea to avoid DOM traversal and listener handling, failed because - * the injected HTML doesn't get styled. See https://github.com/angular/angular/issues/7845 for reference. + * Component offering markdown action buttons for a textarea. + * Associated via input attribute containing the textare's HTML id. */ -export class MarkdownToolbarComponent implements AfterViewInit, OnDestroy { +export class MarkdownToolbarComponent { /** * HTML id associated to the marked up textarea */ @@ -19,16 +18,6 @@ export class MarkdownToolbarComponent implements AfterViewInit, OnDestroy { * Associated textarea */ textarea: HTMLTextAreaElement; - /** - * Cursor position or start of text selection in associated textarea - * @type {number} - */ - private selectionStart = 0; - /** - * Cursor position or end of text selection in associated textarea - * @type {number} - */ - private selectionEnd = 0; /** * Button configuration array used for template generation. See {@link Button} for further information. @@ -46,55 +35,51 @@ export class MarkdownToolbarComponent implements AfterViewInit, OnDestroy { new Button('image', 'Image', 'insert_photo', ''), ]; - ngAfterViewInit() { - // Set 0-timeout to prevent 'ExpressionChangedAfterItHasBeenCheckedError' - // See documentation (https://angular.io/guide/component-interaction#!#parent-to-view-child) for reference. - setTimeout(() => { - // Get the textarea by the id passed to the component - this.textarea = document.getElementById(this.textareaId) as HTMLTextAreaElement; - if (!this.textarea) { - console.log(`MarkdownToolbar: textarea with id '${this.textareaId}' not found.`); - return; - } - // Set blur listener to store the cursor's position (or text selection) so we can add the markdown when pressing an action button - this.textarea.addEventListener('blur', (event) => this.updateSelection(event), true); - }, 0); - } - - ngOnDestroy() { - // Remove blur listener - this.textarea.removeEventListener('blur'); - } - /** - * Called by the template, run action when a button gets pressed + * Gets called in template, run action when a button gets pressed * @param $event * @param button */ onClick($event, button) { + // Get the associated textarea element. See function documentation for further information + this.updateTextarea(); + + // Prevent all default actions (form submission, ..) + $event.preventDefault(); + if (!this.textarea) { + // Cancel the action click in case there is no associated textarea + console.log(`MarkdownToolbar: textarea with id '${this.textareaId}' not found.`); + return; + } + + // Get the textarea's text selection positions (no selection when selectionStart == selectionEnd) + const selectionStart = this.textarea.selectionStart; + const selectionEnd = this.textarea.selectionEnd; + // Get textarea's value const text = this.textarea.value; // Insert the action's text at the cursor's position - this.textarea.value = [text.slice(0, this.selectionStart), button.textBefore, text.slice(this.selectionStart, this.selectionEnd), - button.textAfter, text.slice(this.selectionEnd)].join(''); + this.textarea.value = [text.slice(0, selectionStart), button.textBefore, text.slice(selectionStart, selectionEnd), + button.textAfter, text.slice(selectionEnd)].join(''); // Focus the textarea this.textarea.focus(); // Calculate the new cursor position (based on the action's text length and the previous cursor position) - const cursorPosition = this.selectionStart + button.textBefore.length; + const cursorPosition = selectionStart + button.textBefore.length; // Set the cursor to the calculated position this.textarea.setSelectionRange(cursorPosition, cursorPosition); - // Prevent all default actions (form submission, ..) - $event.preventDefault(); } /** - * Update the cursor's position (or text selection) when the textarea gets blurred. - * Called by 'blur' listener. - * @param event + * Get the textarea by its id in case it isn't already initialized. + * The angular material tab element uses portal hosts to keep the DOM free of inactive tabs which makes it impossible to retrieve the + * element once in the `AfterViewInit`. As this component gets used in the tab element we retrieve the textarea on button clicks as this + * makes sure the associated area is in the DOM too. + * See https://github.com/angular/material2/issues/731 for reference. */ - updateSelection(event) { - this.selectionStart = event.target.selectionStart; - this.selectionEnd = event.target.selectionEnd; + private updateTextarea() { + if (!this.textarea) { + this.textarea = document.getElementById(this.textareaId) as HTMLTextAreaElement; + } } }