Skip to content
Snippets Groups Projects
text-area-math.component.ts 7.73 KiB
Newer Older
import {
  Component, ElementRef,
  Input, OnInit, Renderer2, ViewChild
} from '@angular/core';
import { TextAreaMathElement } from 'common/models/elements/input-elements/text-area-math';
import { MathInputComponent } from 'common/math-editor.module';
import { FormElementComponent } from 'common/directives/form-element-component.directive';
import { OverlayContainer } from '@angular/cdk/overlay';

@Component({
  selector: 'aspect-text-area-math',
  template: `
    <button #insertFormulaButton
            mat-icon-button [matTooltip]="'Formel einfügen'"
            [matTooltipPosition]="'above'"
            cdkOverlayOrigin #trigger="cdkOverlayOrigin"
            [disabled]="!textareaIsFocused"
            (click)="toggleFormulaOverlay()">
      <mat-icon>functions</mat-icon>
    </button>
    <ng-template cdkConnectedOverlay
                 [cdkConnectedOverlayDisableClose]="true"
                 [cdkConnectedOverlayOrigin]="trigger"
                 [cdkConnectedOverlayOpen]="isOverlayOpen">
      <div class="formula-overlay"
           cdkDrag>
        Formeleingabe
        <aspect-mathlive-math-field #mathfield>
        </aspect-mathlive-math-field>
        <div [style.display]="'flex'" [style.flex-direction]="'row'">
          <button mat-button (click)="onConfirmFormula()">
            {{'confirm' | translate }}
          </button>
          <button mat-button (click)="cancelFormula()">
            {{'cancel' | translate }}
          </button>
        </div>
      </div>
    </ng-template>
    <!-- Always set min-height to rowCount -->
    <!-- Fix height when set, so that area scrolls -->
    <div class="textarea"
         [style.min-height.px]="(elementModel.styling.fontSize * (elementModel.styling.lineHeight / 100)) *
                                elementModel.rowCount"
         [style.height.px]="!elementModel.hasAutoHeight &&
                            (elementModel.styling.fontSize * (elementModel.styling.lineHeight / 100))
                             * elementModel.rowCount">
      <span #textarea contenteditable="true"
            [innerHTML]="(savedValue ? savedValue : elementModel.value) | safeResourceHTML"
            (keydown)="elementModel.readOnly ? $event.preventDefault() : null"
            (paste)="elementModel.readOnly ? $event.preventDefault() : null"
            [style.display]="'block'"
            [style.overflow-y]="!elementModel.hasAutoHeight && 'auto'"
            [style.background-color]="elementModel.styling.backgroundColor"
            [style.line-height.%]="elementModel.styling.lineHeight"
            [style.color]="elementModel.styling.fontColor"
            [style.font-family]="elementModel.styling.font"
            [style.font-size.px]="elementModel.styling.fontSize"
            [style.font-weight]="elementModel.styling.bold ? 'bold' : ''"
            [style.font-style]="elementModel.styling.italic ? 'italic' : ''"
            [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''"
            (focus)="textareaIsFocused=true"
            (blur)="onBlur($event)"
            (input)="elementFormControl.setValue(textArea.nativeElement.innerHTML)">
      </span>
    </div>
    <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched">
      {{elementFormControl.errors | errorTransform: elementModel}}
    </mat-error>
  `,
  styles: [
    ':host {display: flex; flex-direction: column; height: 100%;}',
    // margin for showing focus-outline
    '.textarea {border: 1px solid black; border-radius: 3px; flex-grow: 1; margin: 1px;}',
    '.textarea span {height: 100%; word-wrap: anywhere;}',
    '.textarea span:focus {outline: 2px solid #3f51b5;}',
    'aspect-mathlive-math-field {font-size: x-large;}',
    ':host ::ng-deep math-field:focus {outline: 2px solid #3f51b5;}',
    '.formula {display: inline;}',
    '.formula-overlay {display: flex; flex-direction: column; background-color: lightgray; padding: 10px;}',
    '.formula-overlay aspect-mathlive-math-field {background-color: white;}'
  ]
})
export class TextAreaMathComponent extends FormElementComponent implements OnInit {
  @Input() elementModel!: TextAreaMathElement;
  @ViewChild('textarea') textArea!: ElementRef;
  @ViewChild('insertFormulaButton', { read: ElementRef }) insertFormulaButton!: ElementRef;
  @ViewChild('mathfield') mathfieldRef!: MathInputComponent;
  savedValue: string = '';
  isOverlayOpen = false;
  range!: Range;
  formulaIndex!: number;
  textareaIsFocused: boolean = false;

  constructor(public elementRef: ElementRef,
              private renderer: Renderer2,
              private overlayContainer: OverlayContainer) {
    super(elementRef);
  }

  ngOnInit(): void {
    super.ngOnInit();
    if (this.parentForm) this.savedValue = this.elementFormControl.value;
  }

  onConfirmFormula(): void {
    this.isOverlayOpen = false;
    this.overlayContainer.getContainerElement().classList.remove('reduced-overlay-index');

    if (this.elementModel.readOnly) return;
    const formula = this.createFormula(this.mathfieldRef.getMathMLValue());
    this.insertFormula(formula);

    this.updateTextArea();
    this.elementFormControl.setValue(this.textArea.nativeElement.innerHTML);
    this.setFocusAndCursor();
  }

  onBlur(event: FocusEvent): void {
    if (event.relatedTarget !== this.insertFormulaButton.nativeElement) {
      this.textareaIsFocused = false;
      this.elementFormControl.markAsTouched();
    }
  }

  private createFormula(newContent: string): Element {
    const formulaWrapper = this.renderer.createElement('span');
    this.renderer.setAttribute(formulaWrapper, 'contenteditable', 'true');
    this.renderer.addClass(formulaWrapper, 'formula');

    const mathElement = this.renderer.createElement('math');
    this.renderer.setStyle(mathElement, 'display', 'inline-block');
    this.renderer.setProperty(mathElement, 'innerHTML', newContent);
    this.renderer.appendChild(formulaWrapper, mathElement);

    return formulaWrapper;
  }

  private insertFormula(formulaWrapper: Element): void {
    const documentFragment = document.createDocumentFragment();
    // Insert space before
    documentFragment.appendChild(document.createTextNode('\xA0'));
    const formulaNode = documentFragment.appendChild(formulaWrapper);
    // Insert space after
    documentFragment.appendChild(document.createTextNode('\xA0'));
    this.range.deleteContents();
    this.range.insertNode(documentFragment);
    const elements = Array.from(this.textArea.nativeElement.childNodes);
    this.formulaIndex = elements.indexOf(formulaNode);
  }

  private setFocusAndCursor(): void {
    const range = document.createRange();
    const selection = window.getSelection() as Selection;
    range.setStart(this.textArea.nativeElement.childNodes[this.formulaIndex], 1);
    selection.removeAllRanges();
    selection.addRange(range);
    this.textArea.nativeElement.focus();
  }

  private updateTextArea(): void {
    this.renderer.setProperty(this.textArea.nativeElement, 'innerHTML', this.textArea.nativeElement.innerHTML);
  }

  toggleFormulaOverlay(): void {
    if (this.isOverlayOpen) {
      this.overlayContainer.getContainerElement().classList.remove('reduced-overlay-index');
    } else {
      this.overlayContainer.getContainerElement().classList.add('reduced-overlay-index');
    }
    this.isOverlayOpen = !this.isOverlayOpen;
    this.setRange();
  }

  private setRange(): void {
    const selection = window.getSelection() as Selection;
    if (selection && selection.rangeCount > 0) {
      this.range = selection.getRangeAt(0);
    }
  }

  cancelFormula() {
    this.isOverlayOpen = false;
    this.overlayContainer.getContainerElement().classList.remove('reduced-overlay-index');
    const selection = window.getSelection() as Selection;
    selection.removeAllRanges();
    selection.addRange(this.range);
    this.textArea.nativeElement.focus();
  }
}