From 15aec701407e9fe9a8f3dd99e4543d0c2c0f4152 Mon Sep 17 00:00:00 2001
From: jojohoch <joachim.hoch@iqb.hu-berlin.de>
Date: Mon, 25 Nov 2024 10:46:22 +0100
Subject: [PATCH] Refactor TextAreaMath

- Replace top layer with inline editor
- Make keypad and keyboard available for TextAreaMath
---
 docs/unit_definition_changelog.txt            |   9 +
 .../input-elements/math-field.component.ts    |   4 +-
 .../text-area-math.component.ts               | 190 ------------------
 .../text-area-math/area-segment.component.ts  |  72 +++++++
 .../area-text-input.component.ts              |  93 +++++++++
 .../text-area-math.component.ts               | 173 ++++++++++++++++
 .../form-element-component.directive.ts       |   4 +
 projects/common/interfaces.ts                 |   4 +-
 projects/common/math-editor.module.ts         | 177 ++++++----------
 .../elements/input-elements/text-area-math.ts |  37 +++-
 .../services/range-selection-service.ts       |  84 ++++++++
 projects/common/shared.module.ts              |   9 +-
 .../preset-value-properties.component.ts      |   4 +-
 .../modules/logging/services/log.service.ts   |   2 +-
 .../element-group-selection.component.ts      |   6 +-
 .../input-group-element.component.html        |   5 -
 .../input-group-element.component.ts          |   2 -
 .../text-input-group-element.component.html   |   8 +
 .../text-input-group-element.component.ts     |   2 +
 .../player-layout/player-layout.component.ts  |   4 +-
 .../directives/text-input-group.directive.ts  | 133 +++++++-----
 ...ment-model-element-code-mapping.service.ts |   2 +
 .../player/src/app/services/input-service.ts  |   5 +-
 .../src/app/services/keyboard.service.ts      |   8 +-
 .../player/src/app/services/keypad.service.ts |   8 +-
 25 files changed, 657 insertions(+), 388 deletions(-)
 delete mode 100644 projects/common/components/input-elements/text-area-math.component.ts
 create mode 100644 projects/common/components/input-elements/text-area-math/area-segment.component.ts
 create mode 100644 projects/common/components/input-elements/text-area-math/area-text-input.component.ts
 create mode 100644 projects/common/components/input-elements/text-area-math/text-area-math.component.ts
 create mode 100644 projects/common/services/range-selection-service.ts

diff --git a/docs/unit_definition_changelog.txt b/docs/unit_definition_changelog.txt
index 03cc6239d..042e4f737 100644
--- a/docs/unit_definition_changelog.txt
+++ b/docs/unit_definition_changelog.txt
@@ -198,3 +198,12 @@ iqb-aspect-definition@1.0.0
 
 - New Element "MarkingPanel"
 - all elements: new property "alias"
+
+- TextAreaMath
+  - new property: inputAssistancePreset
+  - new property: inputAssistancePosition
+  - new property: inputAssistanceFloatingStartPosition
+  - new property: showSoftwareKeyboard
+  - new property: hideNativeKeyboard
+  - new property: addInputAssistanceToKeyboard
+  - new property: hasArrowKeys
diff --git a/projects/common/components/input-elements/math-field.component.ts b/projects/common/components/input-elements/math-field.component.ts
index 55a46e1f3..e9d1c7c50 100644
--- a/projects/common/components/input-elements/math-field.component.ts
+++ b/projects/common/components/input-elements/math-field.component.ts
@@ -17,12 +17,12 @@ import { MathFieldElement } from 'common/models/elements/input-elements/math-fie
        [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''"
        [style.background-color]="elementModel.styling.backgroundColor">
       <label>{{elementModel.label}}</label><br>
-      <aspect-mathlive-math-field [value]="$any(elementModel.value) | getValue: elementFormControl.value : parentForm"
+      <aspect-math-input [value]="$any(elementModel.value) | getValue: elementFormControl.value : parentForm"
                                   [readonly]="elementModel.readOnly"
                                   [enableModeSwitch]="elementModel.enableModeSwitch"
                                   (input)="elementFormControl.setValue($any($event.target).value)"
                                   (focusout)="elementFormControl.markAsTouched()">
-      </aspect-mathlive-math-field>
+      </aspect-math-input>
       <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched"
                  class="error-message">
         {{elementFormControl.errors | errorTransform: elementModel}}
diff --git a/projects/common/components/input-elements/text-area-math.component.ts b/projects/common/components/input-elements/text-area-math.component.ts
deleted file mode 100644
index a69479ea6..000000000
--- a/projects/common/components/input-elements/text-area-math.component.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-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();
-  }
-}
diff --git a/projects/common/components/input-elements/text-area-math/area-segment.component.ts b/projects/common/components/input-elements/text-area-math/area-segment.component.ts
new file mode 100644
index 000000000..e4734a675
--- /dev/null
+++ b/projects/common/components/input-elements/text-area-math/area-segment.component.ts
@@ -0,0 +1,72 @@
+import {
+  Component, EventEmitter, Input, Output, ViewChild
+} from '@angular/core';
+import { MathEditorModule, MathInputComponent } from 'common/math-editor.module';
+import { AreaTextInputComponent } from 'common/components/input-elements/text-area-math/area-text-input.component';
+import { BehaviorSubject } from 'rxjs';
+
+@Component({
+  selector: 'aspect-text-area-math-segment',
+  template: `
+    @if (type === 'math') {
+      <aspect-math-input #inputComponent
+                                  [fullWidth]="false"
+                                  [value]="value"
+                                  (focusIn)="selectedFocus.next(this.index)"
+                                  (valueChange)="valueChanged.emit({ index: index, value: $event})">
+      </aspect-math-input>
+    } @else {
+      <aspect-area-input #inputComponent
+                         [value]="value"
+                         (focusIn)="onFocusIn($event)"
+                         (focusOut)="onFocusOut($event)"
+                         (onKeyDown)="onKeyDown.emit($event)"
+                         (valueChanged)="valueChanged.emit({ index: index, value: $event})"
+                         (remove)="onRemove($event)">
+      </aspect-area-input>
+    }
+  `,
+  standalone: true,
+  imports: [
+    AreaTextInputComponent,
+    MathEditorModule
+  ],
+  styles: []
+})
+export class AreaSegmentComponent {
+  @Input() type!: 'text' | 'math';
+  @Input() value!: string;
+  @Input() index!: number;
+  @Input() selectedFocus!: BehaviorSubject<number>;
+  @Output() valueChanged: EventEmitter<{ index: number; value: string }> = new EventEmitter();
+  @Output() remove: EventEmitter<number> = new EventEmitter();
+  @Output() focusIn: EventEmitter<HTMLElement> = new EventEmitter();
+  @Output() focusOut: EventEmitter<HTMLElement> = new EventEmitter();
+  @Output() onKeyDown = new EventEmitter<{
+    keyboardEvent: KeyboardEvent;
+    inputElement: HTMLElement;
+  }>();
+
+  @ViewChild('inputComponent') inputComponent!: AreaTextInputComponent | MathInputComponent;
+
+  setFocus(offset?: number) {
+    // if (document.activeElement instanceof HTMLElement) {
+    //   document.activeElement.blur();
+    // }
+    this.inputComponent.setFocus(offset);
+  }
+
+  onFocusIn(input: HTMLElement) {
+    this.selectedFocus.next(this.index);
+    this.focusIn.emit(input);
+  }
+
+  onFocusOut(input: HTMLElement) {
+    this.focusOut.emit(input);
+  }
+
+  onRemove(key: 'Delete' | 'Backspace') {
+    const target = key === 'Backspace' ? this.index - 1 : this.index + 1;
+    this.remove.emit(target);
+  }
+}
diff --git a/projects/common/components/input-elements/text-area-math/area-text-input.component.ts b/projects/common/components/input-elements/text-area-math/area-text-input.component.ts
new file mode 100644
index 000000000..1ed3ead77
--- /dev/null
+++ b/projects/common/components/input-elements/text-area-math/area-text-input.component.ts
@@ -0,0 +1,93 @@
+import {
+  Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild
+} from '@angular/core';
+import { RangeSelectionService } from 'common/services/range-selection-service';
+
+@Component({
+  selector: 'aspect-area-input',
+  template: `
+    <span #inputRef class="input"
+          [style.display]="displayType"
+          [contentEditable]="true"
+          [textContent]="value"
+          (focusin)="onFocusIn(inputRef)"
+          (focusout)="onFocusOut(inputRef)"
+          (blur)="onFocusOut(inputRef)"
+          (keydown)="keyDown($event)"
+          (keyup)="keyUp($event)"
+          (input)="onInput()"></span>
+  `,
+  styles: [`
+    .input {
+      padding: 0 14px;
+      outline: none;
+      white-space: pre-line;
+    }
+  `],
+  standalone: true
+})
+export class AreaTextInputComponent implements OnInit {
+  removePressed: boolean = false;
+  displayType: string = 'inline-block';
+
+  @Input() value!: string;
+  @Output() valueChanged: EventEmitter<string> = new EventEmitter();
+  @Output() focusIn: EventEmitter<HTMLElement> = new EventEmitter();
+  @Output() focusOut: EventEmitter<HTMLElement> = new EventEmitter();
+  @Output() remove: EventEmitter<'Delete' | 'Backspace'> = new EventEmitter();
+  @Output() onKeyDown = new EventEmitter<{
+    keyboardEvent: KeyboardEvent;
+    inputElement: HTMLElement;
+  }>();
+
+  @ViewChild('inputRef') inputRef!: ElementRef;
+
+  ngOnInit(): void {
+    this.setDisplayType(this.value);
+  }
+
+  setFocus(offset?: number) {
+    if (offset) {
+      const range = new Range();
+      const sel = window.getSelection();
+      range.setStart(this.inputRef.nativeElement.firstChild, offset);
+      range.setEnd(this.inputRef.nativeElement.firstChild, offset);
+      sel?.removeAllRanges();
+      sel?.addRange(range);
+      this.inputRef.nativeElement.focus();
+    } else {
+      this.inputRef.nativeElement.focus();
+    }
+  }
+
+  onInput() {
+    this.valueChanged.emit(this.inputRef.nativeElement.textContent);
+    this.setDisplayType(this.inputRef.nativeElement.textContent);
+  }
+
+  private setDisplayType(value: string) {
+    // Fix cursor in empty input in chromium
+    this.displayType = value ? 'inline' : 'inline-block';
+  }
+
+  keyDown(event: KeyboardEvent) {
+    const range = RangeSelectionService.getRange();
+    this.removePressed = !!((event.key === 'Backspace' && range && range.startOffset === 0 && range.endOffset === 0) ||
+      (event.key === 'Delete' && range && range.startOffset === range.endOffset &&
+        range.endOffset === range.endContainer.textContent?.length));
+    this.onKeyDown.emit({ keyboardEvent: event, inputElement: this.inputRef.nativeElement });
+  }
+
+  keyUp(e: KeyboardEvent): void {
+    if (this.removePressed) this.remove.emit(e.key as 'Delete' | 'Backspace');
+    this.removePressed = false;
+  }
+
+  onFocusIn(input: HTMLElement) {
+    this.focusIn.emit(input);
+  }
+
+  onFocusOut(input: HTMLElement) {
+    this.focusOut.emit(input);
+  }
+}
diff --git a/projects/common/components/input-elements/text-area-math/text-area-math.component.ts b/projects/common/components/input-elements/text-area-math/text-area-math.component.ts
new file mode 100644
index 000000000..758ea4c31
--- /dev/null
+++ b/projects/common/components/input-elements/text-area-math/text-area-math.component.ts
@@ -0,0 +1,173 @@
+import {
+  Component, ElementRef, EventEmitter, Input, OnInit, Output, QueryList, ViewChild, ViewChildren
+} from '@angular/core';
+import { TextAreaMathElement, TextAreaMath } from 'common/models/elements/input-elements/text-area-math';
+import { FormElementComponent } from 'common/directives/form-element-component.directive';
+import {
+  AreaSegmentComponent
+} from 'common/components/input-elements/text-area-math/area-segment.component';
+import { BehaviorSubject } from 'rxjs';
+import { RangeSelectionService } from 'common/services/range-selection-service';
+
+@Component({
+  selector: 'aspect-text-area-math',
+  template: `
+    <button class="insert-formula-button"
+            mat-button
+            cdkOverlayOrigin #trigger="cdkOverlayOrigin"
+            (click)="addFormula()">
+      Formel einfügen
+    </button>
+    <div #textArea class="text-area"
+         [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"
+         [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' : ''">
+      <span class="alignment-fix">&nbsp;</span>
+      @for (segment of segments; track segment; let i = $index) {
+        <aspect-text-area-math-segment
+          [style.display]="'inline'"
+          [type]="segment.type"
+          [value]="segment.value"
+          [selectedFocus]="selectedFocus"
+          (valueChanged)="onValueChanged($event)"
+          [index]="i"
+          (focusIn)="focusChanged.emit({ inputElement: $event, focused: true })"
+          (focusOut)="focusChanged.emit({ inputElement: $event, focused: false })"
+          (onKeyDown)="onKeyDown.emit($event)"
+          (remove)="removeSegment($event)">
+        </aspect-text-area-math-segment>
+      }
+    </div>
+    <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched">
+      {{ elementFormControl.errors | errorTransform: elementModel }}
+    </mat-error>
+  `,
+  styles: [
+    '.alignment-fix {padding: 15px 0; display: inline-block; width: 0;}',
+    '.text-area {border: 1px solid black; border-radius: 3px; padding: 5px;}',
+    ':host {display: flex; flex-direction: column; height: 100%;}',
+    '.insert-formula-button {font-size: large; width: 160px; background-color: #ddd; padding: 15px 10px; height: 55px;}'
+  ]
+})
+export class TextAreaMathComponent extends FormElementComponent implements OnInit {
+  @Input() elementModel!: TextAreaMathElement;
+  @Output() mathInputFocusIn: EventEmitter<FocusEvent> = new EventEmitter();
+  @Output() mathInputFocusOut: EventEmitter<FocusEvent> = new EventEmitter();
+  @Output() focusChanged = new EventEmitter<{ inputElement: HTMLElement; focused: boolean }>();
+  @Output() onKeyDown = new EventEmitter<{
+    keyboardEvent: KeyboardEvent;
+    inputElement: HTMLElement;
+  }>();
+
+  @ViewChildren(AreaSegmentComponent) segmentComponents!: QueryList<AreaSegmentComponent>;
+  @ViewChild('textArea') textArea!: ElementRef;
+
+  segments: TextAreaMath[] = [];
+  selectedFocus: BehaviorSubject<number> = new BehaviorSubject<number>(0);
+
+  ngOnInit(): void {
+    super.ngOnInit();
+    if (this.parentForm) this.segments = this.elementFormControl.value;
+    if (this.segments.length === 0) {
+      this.segments.push({ type: 'text', value: '' });
+      super.setElementValue(this.segments);
+    }
+  }
+
+  addFormula() {
+    this.segments = this.elementFormControl.value;
+    this.updateFocus(this.selectedFocus.value);
+    const range = RangeSelectionService.getRange();
+    if (!range) return;
+
+    const segmentIndex = this.selectedFocus.value;
+    let newSegmentIndex = segmentIndex + 1;
+    const selectedType = this.segments[this.selectedFocus.value].type;
+    if (selectedType === 'text') {
+      const content = range.endContainer.parentElement?.textContent || '';
+      const { start, end } = RangeSelectionService
+        .getSelectionRange(range, this.segmentComponents.toArray()[segmentIndex].inputComponent.inputRef.nativeElement);
+      if (content.length === start) {
+        this.addSegments(false, true, segmentIndex, '', '');
+      } else {
+        const startContent = content.slice(0, start);
+        const endContent = content.slice(end);
+        this.segments[segmentIndex].value = startContent;
+        this.addSegments(false, true, segmentIndex, '', endContent);
+      }
+    } else {
+      newSegmentIndex += 1;
+      this.addSegments(true, false, segmentIndex, '', '');
+    }
+    super.setElementValue(this.segments);
+    setTimeout(() => this.updateFocus(newSegmentIndex), 250);
+  }
+
+  private updateFormControl(value: TextAreaMath[]): void {
+    super.setElementValue(value);
+  }
+
+  private addSegments(
+    addStartContent: boolean,
+    addEndContent: boolean,
+    segmentIndex: number,
+    startContent: string,
+    endContent: string) {
+    const targetSegmentIndex = addStartContent ? segmentIndex + 2 : segmentIndex + 1;
+    if (addStartContent) this.segments.splice(targetSegmentIndex - 1, 0, { type: 'text', value: startContent });
+    this.segments.splice(targetSegmentIndex, 0, { type: 'math', value: '' });
+    if (addEndContent) this.segments.splice(targetSegmentIndex + 1, 0, { type: 'text', value: endContent });
+  }
+
+  removeSegment(index: number) {
+    const segments: TextAreaMath[] = [...this.elementFormControl.value];
+    if (segments[index] && segments[index].type === 'math') {
+      segments.splice(index, 1);
+      // combine text segments
+      segments[index - 1].value += segments[index].value;
+      const offset = segments[index - 1].value.length;
+      segments.splice(index, 1);
+      this.segments = segments;
+      super.setElementValue(this.segments);
+      // wait for rendering of segments
+      setTimeout(() => this.updateFocus(index - 1, offset));
+    }
+  }
+
+  onValueChanged(value: { index: number; value: string }): void {
+    const segments = [...this.elementFormControl.value];
+    super.setElementValue(
+      segments
+        .map((segment, index) => ({
+          ...value.index === index ? { type: segment.type, value: value.value } : segment
+        })));
+  }
+
+  private setSegmentValue(value: { index: number; value: string }) {
+    this.segments[value.index].value = value.value;
+    this.updateFormControl(this.segments);
+  }
+
+  setElementValue(value: string, remove?: boolean): void {
+    if (remove) {
+      this.removeSegment(this.selectedFocus.value - 1);
+    } else {
+      this.setSegmentValue({ index: this.selectedFocus.value, value: value });
+    }
+  }
+
+  private updateFocus(index: number, offset?: number): void {
+    this.segmentComponents.toArray()[index].setFocus(offset);
+  }
+}
diff --git a/projects/common/directives/form-element-component.directive.ts b/projects/common/directives/form-element-component.directive.ts
index 476bae77e..6b7e8c106 100644
--- a/projects/common/directives/form-element-component.directive.ts
+++ b/projects/common/directives/form-element-component.directive.ts
@@ -12,4 +12,8 @@ export abstract class FormElementComponent extends ElementComponent implements O
       this.parentForm.controls[this.elementModel.id] as UntypedFormControl :
       new UntypedFormControl(this.elementModel.value);
   }
+
+  setElementValue(value: unknown, option?: unknown): void {
+    this.elementFormControl.setValue(value);
+  }
 }
diff --git a/projects/common/interfaces.ts b/projects/common/interfaces.ts
index 743ccd4ba..fffaa90ea 100644
--- a/projects/common/interfaces.ts
+++ b/projects/common/interfaces.ts
@@ -11,7 +11,8 @@ import {
 import { VisibilityRule } from 'common/models/visibility-rule';
 import { UIElement } from 'common/models/elements/element';
 import { MathTableRow } from 'common/models/elements/input-elements/math-table';
-import { Markable } from 'player/src/app/models/markable.interface'; // TODO
+import { Markable } from 'player/src/app/models/markable.interface';
+import { TextAreaMath } from 'common/models/elements/input-elements/text-area-math';
 
 export type UIElementType =
   'text'
@@ -88,6 +89,7 @@ export type InputElementValue =
   | TextLabel[]
   | Hotspot[]
   | MathTableRow[]
+  | TextAreaMath[]
   | GeometryValue
   | string[]
   | string
diff --git a/projects/common/math-editor.module.ts b/projects/common/math-editor.module.ts
index 7da9ccf15..edc3fa49d 100644
--- a/projects/common/math-editor.module.ts
+++ b/projects/common/math-editor.module.ts
@@ -1,15 +1,14 @@
 // eslint-disable-next-line max-classes-per-file
 import {
   NgModule, CUSTOM_ELEMENTS_SCHEMA,
-  Component, AfterViewInit, ViewChild, ElementRef, Input, OnChanges, SimpleChanges
+  Component, AfterViewInit, ViewChild, ElementRef, Input, OnChanges, SimpleChanges, Output, EventEmitter
 } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { MathfieldElement, VirtualKeyboardDefinition } from 'mathlive';
-import { VirtualKeyboardLayer } from 'mathlive/dist/public/options';
+import { MathfieldElement } from 'mathlive';
 import { MatButtonToggleChange, MatButtonToggleModule } from '@angular/material/button-toggle';
 
 @Component({
-  selector: 'aspect-mathlive-math-field',
+  selector: 'aspect-math-input',
   template: `
     <mat-button-toggle-group *ngIf="enableModeSwitch"
                              [value]="mathFieldElement.mode"
@@ -17,15 +16,36 @@ import { MatButtonToggleChange, MatButtonToggleModule } from '@angular/material/
       <mat-button-toggle value="math">Formel</mat-button-toggle>
       <mat-button-toggle value="text">Text</mat-button-toggle>
     </mat-button-toggle-group>
-    <div #mathfield [class.read-only]="readonly">
+    <div #inputRef
+         (input)="onInput()"
+         [class.full-width]="fullWidth"
+         [class.inline-block]="!fullWidth"
+         [class.read-only]="readonly"
+         (focusin)="onFocusIn($event)"
+         (focusout)="onFocusOut($event)">
     </div>
   `,
   styles: [`
     mat-button-toggle-group {
       height: auto;
     }
+    :host ::ng-deep .full-width math-field {
+      display: block;
+    }
+
+    .inline-block{
+      display: inline-block;
+    }
+
+    :host ::ng-deep  math-field::part(virtual-keyboard-toggle) {
+      display: none;
+    }
+    :host ::ng-deep math-field::part(menu-toggle) {
+      display: none;
+    }
     :host ::ng-deep .read-only math-field {
-      outline: unset; border: unset;
+      outline: unset;
+      border: unset;
     }
     :host ::ng-deep .mat-button-toggle-label-content {
       line-height: unset;
@@ -34,147 +54,66 @@ import { MatButtonToggleChange, MatButtonToggleModule } from '@angular/material/
 })
 export class MathInputComponent implements AfterViewInit, OnChanges {
   @Input() value!: string;
+  @Input() fullWidth: boolean = true;
   @Input() readonly: boolean = false;
   @Input() enableModeSwitch: boolean = false;
-  @ViewChild('mathfield') mathfieldRef!: ElementRef;
+  @Output() valueChange: EventEmitter<string> = new EventEmitter();
+  @Output() focusIn: EventEmitter<FocusEvent> = new EventEmitter();
+  @Output() focusOut: EventEmitter<FocusEvent> = new EventEmitter();
+  @ViewChild('inputRef') inputRef!: ElementRef;
+  @ViewChild('container') container!: ElementRef;
+
+  protected readonly window = window;
 
   mathFieldElement: MathfieldElement = new MathfieldElement({
-    virtualKeyboardMode: 'onfocus',
-    customVirtualKeyboardLayers: MathInputComponent.setupKeyboadLayer(),
-    customVirtualKeyboards: MathInputComponent.setupKeyboard(),
-    virtualKeyboards: 'aspect-keyboard roman greek',
-    keypressSound: null,
-    plonkSound: null,
-    decimalSeparator: ',',
-    smartFence: false
-    // defaultMode: 'math'
+    mathVirtualKeyboardPolicy: 'manual'
   });
 
+  constructor(public elementRef: ElementRef) { }
+
   ngAfterViewInit(): void {
     this.setupMathfield();
   }
 
-  setupMathfield(): void {
-    this.mathfieldRef.nativeElement.appendChild(this.mathFieldElement);
+  private setupMathfield(): void {
+    this.inputRef.nativeElement.appendChild(this.mathFieldElement);
     this.mathFieldElement.value = this.value;
     this.mathFieldElement.readOnly = this.readonly;
   }
 
-  static setupKeyboard(): Record<string, VirtualKeyboardDefinition> {
-    return {
-      'aspect-keyboard': {
-        label: 'Formel', // Label displayed in the Virtual Keyboard Switcher
-        tooltip: 'Zahlen & Formeln', // Tooltip when hovering over the label
-        layer: 'aspect-keyboard-layer'
-      }
-    };
-  }
-
-  static setupKeyboadLayer(): Record<string, string | Partial<VirtualKeyboardLayer>> {
-    return {
-      'aspect-keyboard-layer': {
-        styles: '',
-        rows: [
-          [
-            { label: '7', key: '7' },
-            { label: '8', key: '8' },
-            { label: '9', key: '9' },
-            { class: 'separator w5' },
-            { latex: '+' },
-            { class: 'separator w5' },
-            { latex: '<' },
-            { latex: '>' },
-            { latex: '\\ne' },
-            { class: 'separator w5' },
-            { latex: '€' },
-            { class: 'separator w5' },
-            { label: '<span><i>x</i>&thinsp;²</span>', insert: '$$#@^{2}$$' },
-            { latex: '$$#@^{#?}' },
-            { latex: '$$#@_{#?}' }
-          ],
-          [
-            { label: '4', latex: '4' },
-            { label: '5', key: '5' },
-            { label: '6', key: '6' },
-            { class: 'separator w5' },
-            { latex: '-' },
-            { class: 'separator w5' },
-            { latex: '\\le' },
-            { latex: '\\ge' },
-            { latex: '\\approx' },
-            { class: 'separator w5' },
-            { latex: '\\%' },
-            { class: 'separator w5' },
-            { class: 'small', latex: '\\frac{#0}{#0}' },
-            { latex: '\\sqrt{#0}', insert: '$$\\sqrt{#0}$$' },
-            { class: 'separator' }
-          ],
-          [
-            { label: '1', key: '1' },
-            { label: '2', key: '2' },
-            { label: '3', key: '3' },
-            { class: 'separator w5' },
-            { latex: '\\times' },
-            { class: 'separator w5' },
-            { latex: '(' },
-            { latex: ')' },
-            { latex: '\\Rightarrow' },
-            { class: 'separator w5' },
-            { latex: '°' },
-            { class: 'separator w5' },
-            { latex: '\\overline' },
-            { class: 'separator' },
-            { class: 'separator' }
-          ],
-          [
-            { label: '0', key: '0' },
-            { latex: ',' },
-            { latex: '=' },
-            { class: 'separator w5' },
-            { latex: '\\div' },
-            { class: 'separator w5' },
-            { latex: '[' },
-            { latex: ']' },
-            { latex: '\\Leftrightarrow' },
-            { class: 'separator w5' },
-            { latex: '\\mid' },
-            { class: 'separator w5' },
-            {
-              class: 'action',
-              label: "<svg><use xlink:href='#svg-arrow-left' /></svg>",
-              command: ['performWithFeedback', 'moveToPreviousChar']
-            },
-            {
-              class: 'action',
-              label: "<svg><use xlink:href='#svg-arrow-right' /></svg>",
-              command: ['performWithFeedback', 'moveToNextChar']
-            },
-            {
-              class: 'action font-glyph bottom right',
-              label: '&#x232b;',
-              command: ['performWithFeedback', 'deleteBackward']
-            }
-          ]
-        ]
-      }
-    };
-  }
-
   ngOnChanges(changes: SimpleChanges): void {
     if (changes.value) {
       this.mathFieldElement.setValue(changes.value.currentValue, { mode: 'text' });
     }
   }
 
+  setFocus(offset?: number): void {
+    this.mathFieldElement.focus();
+  }
+
   setParseMode(event: MatButtonToggleChange) {
     // TODO Keyboard moving up and down on focus loss may be avoided by using useSharedVirtualKeyboard
     this.mathFieldElement.mode = event.value;
-    (this.mathfieldRef.nativeElement.childNodes[0] as HTMLElement).focus();
+    (this.inputRef.nativeElement.childNodes[0] as HTMLElement).focus();
   }
 
   getMathMLValue(): string {
     return this.mathFieldElement.getValue('math-ml');
   }
+
+  onInput() {
+    this.valueChange.emit(this.mathFieldElement.getValue());
+  }
+
+  onFocusIn(event: FocusEvent) {
+    this.focusIn.emit(event);
+    window.mathVirtualKeyboard.show();
+  }
+
+  onFocusOut(event: FocusEvent) {
+    window.mathVirtualKeyboard.hide();
+    this.focusOut.emit(event);
+  }
 }
 
 @NgModule({
diff --git a/projects/common/models/elements/input-elements/text-area-math.ts b/projects/common/models/elements/input-elements/text-area-math.ts
index aba3fc238..7f3d3e22c 100644
--- a/projects/common/models/elements/input-elements/text-area-math.ts
+++ b/projects/common/models/elements/input-elements/text-area-math.ts
@@ -9,16 +9,29 @@ import {
 import { Type } from '@angular/core';
 import { ElementComponent } from 'common/directives/element-component.directive';
 import { VariableInfo } from '@iqb/responses';
-import { TextAreaMathComponent } from 'common/components/input-elements/text-area-math.component';
+import { TextAreaMathComponent } from 'common/components/input-elements/text-area-math/text-area-math.component';
 import { environment } from 'common/environment';
-import { AbstractIDService, InputElementProperties, UIElementType } from 'common/interfaces';
+import {
+  AbstractIDService,
+  InputAssistancePreset,
+  InputElementProperties,
+  KeyInputElementProperties,
+  UIElementType
+} from 'common/interfaces';
 import { InstantiationEror } from 'common/errors';
 
 export class TextAreaMathElement extends InputElement implements TextAreaMathProperties {
   type: UIElementType = 'text-area-math';
-  value = '';
+  value: TextAreaMath[] = [];
   rowCount: number = 2;
   hasAutoHeight: boolean = false;
+  inputAssistancePreset: InputAssistancePreset = null;
+  inputAssistancePosition: 'floating' | 'right' = 'floating';
+  inputAssistanceFloatingStartPosition: 'startBottom' | 'endCenter' = 'startBottom';
+  showSoftwareKeyboard: boolean = false;
+  addInputAssistanceToKeyboard: boolean = false;
+  hideNativeKeyboard: boolean = false;
+  hasArrowKeys: boolean = false;
   position: PositionProperties;
   styling: BasicStyles & {
     lineHeight: number;
@@ -32,15 +45,23 @@ export class TextAreaMathElement extends InputElement implements TextAreaMathPro
     if (isTextAreaMathProperties(element)) {
       this.rowCount = element.rowCount;
       this.hasAutoHeight = element.hasAutoHeight;
+      this.inputAssistancePreset = element.inputAssistancePreset;
+      this.inputAssistancePosition = element.inputAssistancePosition;
+      this.inputAssistanceFloatingStartPosition = element.inputAssistanceFloatingStartPosition;
+      this.showSoftwareKeyboard = element.showSoftwareKeyboard;
+      this.addInputAssistanceToKeyboard = element.addInputAssistanceToKeyboard;
+      this.hideNativeKeyboard = element.hideNativeKeyboard;
+      this.hasArrowKeys = element.hasArrowKeys;
       this.position = { ...element.position };
       this.styling = { ...element.styling };
     } else {
       if (environment.strictInstantiation) {
         throw new InstantiationEror('Error at TextAreaMath instantiation', element);
       }
-      if (element?.value !== undefined) this.value = element?.value as string;
+      if (element?.value !== undefined) this.value = element?.value as TextAreaMath[] || [];
       if (element?.rowCount !== undefined) this.rowCount = element.rowCount;
       if (element?.hasAutoHeight !== undefined) this.hasAutoHeight = element.hasAutoHeight;
+      Object.assign(this, PropertyGroupGenerators.generateKeyInputProps(element));
       this.dimensions = PropertyGroupGenerators.generateDimensionProps(element?.dimensions);
       this.position = PropertyGroupGenerators.generatePositionProps(element?.position);
       this.styling = {
@@ -70,7 +91,7 @@ export class TextAreaMathElement extends InputElement implements TextAreaMathPro
   }
 }
 
-export interface TextAreaMathProperties extends InputElementProperties {
+export interface TextAreaMathProperties extends InputElementProperties, KeyInputElementProperties {
   rowCount: number;
   hasAutoHeight: boolean;
   position: PositionProperties;
@@ -79,11 +100,17 @@ export interface TextAreaMathProperties extends InputElementProperties {
   };
 }
 
+export interface TextAreaMath {
+  type: 'text' | 'math';
+  value: string
+}
+
 function isTextAreaMathProperties(blueprint?: Partial<TextAreaMathProperties>): blueprint is TextAreaMathProperties {
   if (!blueprint) return false;
   return blueprint.rowCount !== undefined &&
     blueprint.hasAutoHeight !== undefined &&
     PropertyGroupValidators.isValidPosition(blueprint.position) &&
     PropertyGroupValidators.isValidBasicStyles(blueprint.styling) &&
+    PropertyGroupValidators.isValidKeyInputElementProperties(blueprint) &&
     blueprint.styling?.lineHeight !== undefined;
 }
diff --git a/projects/common/services/range-selection-service.ts b/projects/common/services/range-selection-service.ts
new file mode 100644
index 000000000..43d391442
--- /dev/null
+++ b/projects/common/services/range-selection-service.ts
@@ -0,0 +1,84 @@
+export class RangeSelectionService {
+  static getRange(): Range | null {
+    const selection = window.getSelection() as Selection;
+    if (selection && selection.rangeCount > 0) {
+      return selection.getRangeAt(0);
+    }
+    return null;
+  }
+
+  static getSelectionRange(range: Range, inputElement: HTMLElement): { start: number; end: number } {
+    let start = 0;
+    let end = 0;
+
+    if (!inputElement.contains(range.commonAncestorContainer)) {
+      return { start, end };
+    }
+
+    const calculateOffsets = (node: Node): number => {
+      let offset = 0;
+      const walker = document.createTreeWalker(inputElement, NodeFilter.SHOW_TEXT, null);
+      let currentNode: Node | null = walker.nextNode();
+
+      while (currentNode) {
+        if (currentNode === node) {
+          break;
+        }
+        offset += currentNode.textContent?.length || 0;
+        currentNode = walker.nextNode();
+      }
+      return offset;
+    };
+
+    start = calculateOffsets(range.startContainer) + range.startOffset;
+    end = calculateOffsets(range.endContainer) + range.endOffset;
+
+    return { start, end };
+  }
+
+  static setSelectionRange(
+    inputElement: HTMLElement,
+    start: number,
+    end: number
+  ): void {
+    const range = new Range();
+    const selection = window.getSelection();
+    if (!selection) return;
+
+    let charCount = 0;
+
+    const setRangeOffsets = (node: Node): boolean => {
+      if (node.nodeType === Node.TEXT_NODE) {
+        const textLength = node.textContent?.length || 0;
+
+        // Prüfen, ob Startpunkt im aktuellen Textknoten liegt
+        if (start >= charCount && start <= charCount + textLength) {
+          range.setStart(node, start - charCount);
+        }
+
+        // Prüfen, ob Endpunkt im aktuellen Textknoten liegt
+        if (end >= charCount && end <= charCount + textLength) {
+          range.setEnd(node, end - charCount);
+          return true; // Fertig, wenn beide Punkte gesetzt sind
+        }
+
+        charCount += textLength;
+      } else {
+        // Rekursive Durchsuchung von Kindknoten
+        const childNodes = node.childNodes;
+        for (let i = 0; i < childNodes.length; i++) {
+          if (setRangeOffsets(childNodes[i])) {
+            return true; // Fertig, wenn beide Punkte gesetzt sind
+          }
+        }
+      }
+
+      return false;
+    };
+
+    setRangeOffsets(inputElement);
+
+    selection.removeAllRanges();
+    selection.addRange(range);
+  }
+}
diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts
index bdb97c6ff..1de52387c 100644
--- a/projects/common/shared.module.ts
+++ b/projects/common/shared.module.ts
@@ -32,6 +32,9 @@ import { PointerEventTooltipDirective } from 'common/components/tooltip/pointer-
 import { ClozeChildErrorMessage } from 'common/components/compound-elements/cloze/cloze-child-error-message';
 import { TriggerComponent } from 'common/components/trigger/trigger.component';
 import { ImageFullscreenDirective } from 'common/directives/image-fullscreen.directive';
+import {
+  AreaSegmentComponent
+} from 'common/components/input-elements/text-area-math/area-segment.component';
 import { TextComponent } from './components/text/text.component';
 import { ButtonComponent } from './components/button/button.component';
 import { TextFieldComponent } from './components/input-elements/text-field.component';
@@ -80,12 +83,11 @@ import { MathDegreesPipe } from './pipes/math-degrees.pipe';
 import { ArrayIncludesPipe } from './pipes/array-includes.pipe';
 import { SpinnerComponent } from './components/spinner/spinner.component';
 import { GetValuePipe, MathFieldComponent } from './components/input-elements/math-field.component';
-import { MeasurePipe } from './pipes/measure.pipe';
 import { TextImagePanelComponent } from './components/text-image-panel.component';
 import { UnitDefErrorDialogComponent } from './components/unit-def-error-dialog.component';
 import { MathTableComponent } from './components/input-elements/math-table.component';
 
-import { TextAreaMathComponent } from './components/input-elements/text-area-math.component';
+import { TextAreaMathComponent } from './components/input-elements/text-area-math/text-area-math.component';
 import { DragImageComponent } from './components/input-elements/drop-list/drag-image.component';
 import { DraggableDirective } from './components/input-elements/drop-list/draggable.directive';
 
@@ -114,7 +116,8 @@ import { DraggableDirective } from './components/input-elements/drop-list/dragga
     CdkConnectedOverlay,
     CdkOverlayOrigin,
     DraggableDirective,
-    ImageFullscreenDirective
+    ImageFullscreenDirective,
+    AreaSegmentComponent
   ],
   declarations: [
     ButtonComponent,
diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/preset-value-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/preset-value-properties.component.ts
index c48407ff7..533e09653 100644
--- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/preset-value-properties.component.ts
+++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/preset-value-properties.component.ts
@@ -40,10 +40,10 @@ import { CombinedProperties } from 'editor/src/app/components/properties-panel/e
 
     <ng-container *ngIf="combinedProperties.type === 'math-field'">
       <mat-label>{{'preset' | translate }}</mat-label><br>
-      <aspect-mathlive-math-field [value]="$any(combinedProperties).value"
+      <aspect-math-input [value]="$any(combinedProperties).value"
                                   [enableModeSwitch]="true"
                                   (input)="updateModel.emit({property: 'value', value: $any($event.target).value })">
-      </aspect-mathlive-math-field>
+      </aspect-math-input>
     </ng-container>
   `
 })
diff --git a/projects/player/modules/logging/services/log.service.ts b/projects/player/modules/logging/services/log.service.ts
index 651ce17b3..47ad6e147 100644
--- a/projects/player/modules/logging/services/log.service.ts
+++ b/projects/player/modules/logging/services/log.service.ts
@@ -6,7 +6,7 @@ export enum LogLevel { NONE = 0, ERROR = 1, WARN = 2, INFO = 3, DEBUG = 4 }
   providedIn: 'root'
 })
 export class LogService {
-  static level: LogLevel = 0;
+  static level: LogLevel = 4;
 
   static error(...args: unknown[]): void {
     if (LogService.level >= LogLevel.ERROR) {
diff --git a/projects/player/src/app/components/elements/element-group-selection/element-group-selection.component.ts b/projects/player/src/app/components/elements/element-group-selection/element-group-selection.component.ts
index 9d1d56af8..f2e4e1505 100644
--- a/projects/player/src/app/components/elements/element-group-selection/element-group-selection.component.ts
+++ b/projects/player/src/app/components/elements/element-group-selection/element-group-selection.component.ts
@@ -1,7 +1,7 @@
 import { Component, Input, OnInit } from '@angular/core';
 import { UIElement } from 'common/models/elements/element';
-import { ElementGroupInterface, ElementGroupName } from '../../../models/element-group.interface';
 import { UIElementType } from 'common/interfaces';
+import { ElementGroupInterface, ElementGroupName } from '../../../models/element-group.interface';
 
 @Component({
   selector: 'aspect-element-group-selection',
@@ -13,13 +13,13 @@ export class ElementGroupSelectionComponent implements OnInit {
   @Input() pageIndex!: number;
 
   groups: ElementGroupInterface[] = [
-    { name: 'textInputGroup', types: ['text-field', 'text-area', 'spell-correct'] },
+    { name: 'textInputGroup', types: ['text-field', 'text-area', 'spell-correct', 'text-area-math'] },
     { name: 'mediaPlayerGroup', types: ['audio', 'video'] },
     {
       name: 'inputGroup',
       types: [
         'checkbox', 'slider', 'drop-list', 'radio', 'radio-group-images',
-        'dropdown', 'hotspot-image', 'math-field', 'text-area-math'
+        'dropdown', 'hotspot-image', 'math-field'
       ]
     },
     { name: 'compoundGroup', types: ['cloze', 'likert', 'table'] },
diff --git a/projects/player/src/app/components/elements/input-group-element/input-group-element.component.html b/projects/player/src/app/components/elements/input-group-element/input-group-element.component.html
index 99be60249..95562c52c 100644
--- a/projects/player/src/app/components/elements/input-group-element/input-group-element.component.html
+++ b/projects/player/src/app/components/elements/input-group-element/input-group-element.component.html
@@ -40,9 +40,4 @@
                      [parentForm]="form"
                      [elementModel]="elementModel | cast: MathFieldElement">
   </aspect-math-field>
-  <aspect-text-area-math *ngIf="elementModel.type === 'text-area-math'"
-                         #elementComponent
-                         [parentForm]="form"
-                         [elementModel]="elementModel | cast: TextAreaMathElement">
-  </aspect-text-area-math>
 </form>
diff --git a/projects/player/src/app/components/elements/input-group-element/input-group-element.component.ts b/projects/player/src/app/components/elements/input-group-element/input-group-element.component.ts
index 6782ee78e..53ddef17a 100644
--- a/projects/player/src/app/components/elements/input-group-element/input-group-element.component.ts
+++ b/projects/player/src/app/components/elements/input-group-element/input-group-element.component.ts
@@ -12,7 +12,6 @@ import { DropdownElement } from 'common/models/elements/input-elements/dropdown'
 import { InputElement } from 'common/models/elements/element';
 import { HotspotImageElement } from 'common/models/elements/input-elements/hotspot-image';
 import { MathFieldElement } from 'common/models/elements/input-elements/math-field';
-import { TextAreaMathElement } from 'common/models/elements/input-elements/text-area-math';
 import { ValidationService } from '../../../services/validation.service';
 import { ElementFormGroupDirective } from '../../../directives/element-form-group.directive';
 import { ElementModelElementCodeMappingService } from '../../../services/element-model-element-code-mapping.service';
@@ -33,7 +32,6 @@ export class InputGroupElementComponent extends ElementFormGroupDirective implem
   DropdownElement!: DropdownElement;
   HotspotImageElement!: HotspotImageElement;
   MathFieldElement!: MathFieldElement;
-  TextAreaMathElement!: TextAreaMathElement;
 
   constructor(
     public unitStateService: UnitStateService,
diff --git a/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.html b/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.html
index d70d391e2..1485b28a8 100644
--- a/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.html
+++ b/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.html
@@ -24,6 +24,14 @@
     (onKeyDown)="detectHardwareKeyboard(elementModel); checkInputLimitation($event, elementModel)"
     (focusChanged)="toggleKeyInput($event, elementComponent)">
   </aspect-spell-correct>
+  <aspect-text-area-math
+    *ngIf="elementModel.type === 'text-area-math'"
+    #elementComponent
+    [parentForm]="form"
+    [elementModel]="elementModel | cast: TextAreaMathElement"
+    (onKeyDown)="detectHardwareKeyboard(elementModel)"
+    (focusChanged)="toggleKeyInput($event, elementComponent)">
+  </aspect-text-area-math>
 </form>
 
 <aspect-floating-keypad
diff --git a/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.ts b/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.ts
index 8bee267e3..e877382e9 100644
--- a/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.ts
+++ b/projects/player/src/app/components/elements/text-input-group-element/text-input-group-element.component.ts
@@ -6,6 +6,7 @@ import { ElementComponent } from 'common/directives/element-component.directive'
 import { TextAreaElement } from 'common/models/elements/input-elements/text-area';
 import { TextFieldElement } from 'common/models/elements/input-elements/text-field';
 import { SpellCorrectElement } from 'common/models/elements/input-elements/spell-correct';
+import { TextAreaMathElement } from 'common/models/elements/input-elements/text-area-math';
 import { InputElement } from 'common/models/elements/element';
 import { TextInputGroupDirective } from 'player/src/app/directives/text-input-group.directive';
 import { DeviceService } from '../../../services/device.service';
@@ -27,6 +28,7 @@ export class TextInputGroupElementComponent
   TextAreaElement!: TextAreaElement;
   TextFieldElement!: TextFieldElement;
   SpellCorrectElement!: SpellCorrectElement;
+  TextAreaMathElement!: TextAreaMathElement;
 
   constructor(
     public keyboardService: KeyboardService,
diff --git a/projects/player/src/app/components/layouts/player-layout/player-layout.component.ts b/projects/player/src/app/components/layouts/player-layout/player-layout.component.ts
index 030208cca..32d3e432a 100644
--- a/projects/player/src/app/components/layouts/player-layout/player-layout.component.ts
+++ b/projects/player/src/app/components/layouts/player-layout/player-layout.component.ts
@@ -64,7 +64,7 @@ export class PlayerLayoutComponent implements OnDestroy {
   }
 
   setIsKeypadToggling(): void {
-    this.isKeypadToggling = setTimeout(() => {
+    this.isKeypadToggling = window.setTimeout(() => {
       clearTimeout(this.isKeypadToggling);
       this.isKeypadToggling = 0;
       this.isKeypadAnimationDisabled = false;
@@ -72,7 +72,7 @@ export class PlayerLayoutComponent implements OnDestroy {
   }
 
   setIsKeyboardToggling(): void {
-    this.isKeyboardToggling = setTimeout(() => {
+    this.isKeyboardToggling = window.setTimeout(() => {
       clearTimeout(this.isKeyboardToggling);
       this.isKeyboardToggling = 0;
       this.isKeyboardAnimationDisabled = false;
diff --git a/projects/player/src/app/directives/text-input-group.directive.ts b/projects/player/src/app/directives/text-input-group.directive.ts
index 8d87f616e..2c02fed1d 100644
--- a/projects/player/src/app/directives/text-input-group.directive.ts
+++ b/projects/player/src/app/directives/text-input-group.directive.ts
@@ -8,11 +8,13 @@ import { DeviceService } from 'player/src/app/services/device.service';
 import { KeypadService } from 'player/src/app/services/keypad.service';
 import { KeyboardService } from 'player/src/app/services/keyboard.service';
 import { TextInputComponentType } from 'player/src/app/models/text-input-component.type';
+import { TextAreaMathComponent } from 'common/components/input-elements/text-area-math/text-area-math.component';
+import { RangeSelectionService } from 'common/services/range-selection-service';
 
 @Directive()
 export abstract class TextInputGroupDirective extends ElementFormGroupDirective implements OnDestroy {
   isKeypadOpen: boolean = false;
-  inputElement!: HTMLTextAreaElement | HTMLInputElement;
+  inputElement!: HTMLTextAreaElement | HTMLInputElement | HTMLElement;
 
   keypadEnterKeySubscription!: Subscription;
   keypadDeleteCharactersSubscription!: Subscription;
@@ -32,7 +34,7 @@ export abstract class TextInputGroupDirective extends ElementFormGroupDirective
   }
 
   async toggleKeyInput(focusedTextInput: { inputElement: HTMLElement; focused: boolean },
-                       elementComponent: TextInputComponentType): Promise<void> {
+                       elementComponent: TextInputComponentType | TextAreaMathComponent): Promise<void> {
     const promises: Promise<boolean>[] = [];
     if (elementComponent.elementModel.showSoftwareKeyboard && !elementComponent.elementModel.readOnly) {
       promises.push(this.keyboardService
@@ -42,22 +44,23 @@ export abstract class TextInputGroupDirective extends ElementFormGroupDirective
       promises.push(this.keypadService.toggleAsync(focusedTextInput, elementComponent));
     }
     if (promises.length) {
-      await Promise.all(promises).then(() => {
-        if (this.keyboardService.isOpen) {
-          this.subscribeForKeyboardEvents(elementComponent.elementModel, elementComponent);
-        } else {
-          this.unsubscribeFromKeyboardEvents();
-        }
-        if (this.keypadService.isOpen) {
-          this.subscribeForKeypadEvents(elementComponent.elementModel, elementComponent);
-        } else {
-          this.unsubscribeFromKeypadEvents();
-        }
-        this.isKeypadOpen = this.keypadService.isOpen;
-        if (this.keyboardService.isOpen || this.keypadService.isOpen) {
-          this.setInputElement(focusedTextInput.inputElement);
-        }
-      });
+      await Promise.all(promises)
+        .then(() => {
+          if (this.keyboardService.isOpen) {
+            this.subscribeForKeyboardEvents(elementComponent.elementModel, elementComponent);
+          } else {
+            this.unsubscribeFromKeyboardEvents();
+          }
+          if (this.keypadService.isOpen) {
+            this.subscribeForKeypadEvents(elementComponent.elementModel, elementComponent);
+          } else {
+            this.unsubscribeFromKeypadEvents();
+          }
+          this.isKeypadOpen = this.keypadService.isOpen;
+          if (this.keyboardService.isOpen || this.keypadService.isOpen) {
+            this.inputElement = this.getInputElement(focusedTextInput.inputElement);
+          }
+        });
     }
   }
 
@@ -108,15 +111,20 @@ export abstract class TextInputGroupDirective extends ElementFormGroupDirective
     if (this.keyboardDeleteCharactersSubscription) this.keyboardDeleteCharactersSubscription.unsubscribe();
   }
 
-  private setInputElement(inputElement: HTMLElement): void {
-    this.inputElement = this.elementModel.type === 'text-area' ?
-      inputElement as HTMLTextAreaElement :
-      inputElement as HTMLInputElement;
+  private getInputElement(inputElement: HTMLElement): HTMLTextAreaElement | HTMLInputElement | HTMLElement {
+    switch (this.elementModel.type) {
+      case 'text-area':
+        return inputElement as HTMLTextAreaElement;
+      case 'text-area-math':
+        return inputElement as HTMLElement;
+      default:
+        return inputElement as HTMLInputElement;
+    }
   }
 
   private select(direction: string): void {
     let lastBreak = 0;
-    const inputValueKeys = this.inputElement.value.split('');
+    const inputValueKeys = this.getInputElementValue().split('');
     const lineBreaks = inputValueKeys
       .reduce(
         (previousValue: number[][],
@@ -132,17 +140,16 @@ export abstract class TextInputGroupDirective extends ElementFormGroupDirective
           }
           return previousValue;
         }, []);
-    const selectionStart = this.inputElement.selectionStart || 0;
-    const selectionEnd = this.inputElement.selectionEnd || 0;
-    let newSelection = selectionStart;
-
+    const selectionStart = this.getSelection().start;
+    const selectionEnd = this.getSelection().end;
+    let newSelection: number;
     switch (direction) {
       case 'ArrowLeft': {
-        newSelection -= 1;
+        newSelection = selectionStart === selectionEnd ? selectionStart - 1 : selectionStart;
         break;
       }
       case 'ArrowRight': {
-        newSelection += 1;
+        newSelection = selectionStart === selectionEnd ? selectionEnd + 1 : selectionEnd;
         break;
       }
       case 'ArrowUp': {
@@ -170,15 +177,15 @@ export abstract class TextInputGroupDirective extends ElementFormGroupDirective
         newSelection = selectionStart;
       }
     }
-    this.inputElement.setSelectionRange(newSelection, newSelection);
+    this.setSelection(newSelection, newSelection);
   }
 
   private enterKey(key: string, elementModel: UIElement, elementComponent: ElementComponent): void {
     if (!(elementModel.maxLength &&
       elementModel.isLimitedToMaxLength &&
-      this.inputElement.value.length === elementModel.maxLength)) {
-      const selectionStart = this.inputElement.selectionStart || 0;
-      const selectionEnd = this.inputElement.selectionEnd || 0;
+      this.getInputElementValue().length === elementModel.maxLength)) {
+      const selectionStart = this.getSelection().start;
+      const selectionEnd = this.getSelection().end;
       const newSelection = selectionStart ? selectionStart + 1 : 1;
       this.insert({
         selectionStart, selectionEnd, newSelection, key
@@ -187,17 +194,17 @@ export abstract class TextInputGroupDirective extends ElementFormGroupDirective
   }
 
   private deleteCharacters(backspace: boolean, elementComponent: ElementComponent): void {
-    let selectionStart = this.inputElement.selectionStart || 0;
-    let selectionEnd = this.inputElement.selectionEnd || 0;
-    if (backspace && selectionEnd > 0) {
-      if (selectionStart === selectionEnd) {
+    let selectionStart = this.getSelection().start;
+    let selectionEnd = this.getSelection().end;
+    if (backspace) {
+      if (selectionStart === selectionEnd && selectionEnd > 0) {
         selectionStart -= 1;
       }
       this.insert({
         selectionStart, selectionEnd, newSelection: selectionStart, key: ''
-      }, elementComponent);
+      }, elementComponent, (backspace && selectionEnd === 0) || undefined);
     }
-    if (!backspace && selectionEnd <= this.inputElement.value.length) {
+    if (!backspace && selectionEnd <= this.getInputElementValue().length) {
       if (selectionStart === selectionEnd) {
         selectionEnd += 1;
       }
@@ -207,17 +214,53 @@ export abstract class TextInputGroupDirective extends ElementFormGroupDirective
     }
   }
 
+  private getSelection(): { start: number; end: number } {
+    if (this.inputElement instanceof HTMLInputElement || this.inputElement instanceof HTMLTextAreaElement) {
+      return { start: this.inputElement.selectionStart || 0, end: this.inputElement.selectionEnd || 0 };
+    }
+    return this.getSelectionRange();
+  }
+
+  private getSelectionRange(): { start: number; end: number } {
+    const range = RangeSelectionService.getRange();
+    if (!range) return { start: 0, end: 0 };
+    return RangeSelectionService.getSelectionRange(range, this.inputElement);
+  }
+
+  private getInputElementValue(): string {
+    if (this.inputElement instanceof HTMLInputElement || this.inputElement instanceof HTMLTextAreaElement) {
+      return this.inputElement.value;
+    }
+    return this.inputElement.textContent || '';
+  }
+
   private insert(keyAtPosition: {
     selectionStart: number;
     selectionEnd: number;
     newSelection: number;
     key: string
-  }, elementComponent: ElementComponent): void {
-    const startText = this.inputElement.value.substring(0, keyAtPosition.selectionStart);
-    const endText = this.inputElement.value.substring(keyAtPosition.selectionEnd);
-    (elementComponent as FormElementComponent).elementFormControl
-      .setValue(startText + keyAtPosition.key + endText);
-    this.inputElement.setSelectionRange(keyAtPosition.newSelection, keyAtPosition.newSelection);
+  }, elementComponent: ElementComponent, backSpaceAtFirstPosition?: boolean): void {
+    const startText = this.getStartText(keyAtPosition.selectionStart);
+    const endText = this.getEndText(keyAtPosition.selectionEnd);
+    (elementComponent as FormElementComponent)
+      .setElementValue(startText + keyAtPosition.key + endText, backSpaceAtFirstPosition || undefined);
+    this.setSelection(keyAtPosition.newSelection, keyAtPosition.newSelection, backSpaceAtFirstPosition || undefined);
+  }
+
+  private getStartText(startPosition: number): string {
+    return this.getInputElementValue().substring(0, startPosition);
+  }
+
+  private getEndText(endPosition: number): string {
+    return this.getInputElementValue().substring(endPosition);
+  }
+
+  setSelection(start: number, end: number, backSpaceAtFirstPosition?: boolean): void {
+    if (this.inputElement instanceof HTMLInputElement || this.inputElement instanceof HTMLTextAreaElement) {
+      this.inputElement.setSelectionRange(start, end);
+    } else if (!backSpaceAtFirstPosition) {
+      setTimeout(() => RangeSelectionService.setSelectionRange(this.inputElement, start, end));
+    }
   }
 
   ngOnDestroy(): void {
diff --git a/projects/player/src/app/services/element-model-element-code-mapping.service.ts b/projects/player/src/app/services/element-model-element-code-mapping.service.ts
index e53144c95..0c424590b 100644
--- a/projects/player/src/app/services/element-model-element-code-mapping.service.ts
+++ b/projects/player/src/app/services/element-model-element-code-mapping.service.ts
@@ -29,6 +29,7 @@ export class ElementModelElementCodeMappingService {
 
   mapToElementModelValue(elementCodeValue: ResponseValueType | undefined, elementModel: UIElement): InputElementValue {
     switch (elementModel.type) {
+      case 'text-area-math':
       case 'math-table':
         return (elementCodeValue !== undefined) ?
           JSON.parse(elementCodeValue as string) :
@@ -89,6 +90,7 @@ export class ElementModelElementCodeMappingService {
         return elementModelValue as string;
       case 'image':
         return elementModelValue as boolean;
+      case 'text-area-math':
       case 'math-table':
         return JSON.stringify(elementModelValue);
       case 'drop-list':
diff --git a/projects/player/src/app/services/input-service.ts b/projects/player/src/app/services/input-service.ts
index d75e6244d..123c0c069 100644
--- a/projects/player/src/app/services/input-service.ts
+++ b/projects/player/src/app/services/input-service.ts
@@ -2,13 +2,14 @@ import { EventEmitter, Injectable, Output } from '@angular/core';
 import { TextInputComponentType } from 'player/src/app/models/text-input-component.type';
 import { MathTableComponent } from 'common/components/input-elements/math-table.component';
 import { InputAssistancePreset } from 'common/interfaces';
+import { TextAreaMathComponent } from 'common/components/input-elements/text-area-math/text-area-math.component';
 
 @Injectable({
   providedIn: 'root'
 })
 export abstract class InputService {
   preset: InputAssistancePreset = null;
-  elementComponent!: TextInputComponentType | MathTableComponent;
+  elementComponent!: TextInputComponentType | MathTableComponent | TextAreaMathComponent;
   inputElement!: HTMLTextAreaElement | HTMLInputElement | HTMLElement;
   isOpen: boolean = false;
 
@@ -19,7 +20,7 @@ export abstract class InputService {
 
   setCurrentKeyInputElement(
     focusedElement: HTMLElement,
-    elementComponent: TextInputComponentType | MathTableComponent
+    elementComponent: TextInputComponentType | MathTableComponent | TextAreaMathComponent
   ): void {
     this.inputElement = focusedElement;
     this.elementComponent = elementComponent;
diff --git a/projects/player/src/app/services/keyboard.service.ts b/projects/player/src/app/services/keyboard.service.ts
index 540a2c959..d48a7e508 100644
--- a/projects/player/src/app/services/keyboard.service.ts
+++ b/projects/player/src/app/services/keyboard.service.ts
@@ -1,6 +1,7 @@
 import { Injectable } from '@angular/core';
 import { TextInputComponentType } from 'player/src/app/models/text-input-component.type';
 import { MathTableComponent } from 'common/components/input-elements/math-table.component';
+import { TextAreaMathComponent } from 'common/components/input-elements/text-area-math/text-area-math.component';
 import { InputService } from './input-service';
 
 @Injectable({
@@ -10,7 +11,7 @@ export class KeyboardService extends InputService {
   addInputAssistanceToKeyboard: boolean = false;
 
   async toggleAsync(focusedTextInput: { inputElement: HTMLElement; focused: boolean },
-                    elementComponent: TextInputComponentType | MathTableComponent,
+                    elementComponent: TextInputComponentType | MathTableComponent | TextAreaMathComponent,
                     isMobileWithoutHardwareKeyboard: boolean): Promise<boolean> {
     this.willToggle.emit(this.isOpen);
     return new Promise(resolve => {
@@ -22,7 +23,7 @@ export class KeyboardService extends InputService {
   }
 
   private toggle(focusedTextInput: { inputElement: HTMLElement; focused: boolean },
-                 elementComponent: TextInputComponentType | MathTableComponent,
+                 elementComponent: TextInputComponentType | MathTableComponent | TextAreaMathComponent,
                  isMobileWithoutHardwareKeyboard: boolean): boolean {
     if (focusedTextInput.focused && isMobileWithoutHardwareKeyboard) {
       this.open(focusedTextInput.inputElement, elementComponent);
@@ -32,7 +33,8 @@ export class KeyboardService extends InputService {
     return this.isOpen;
   }
 
-  open(inputElement: HTMLElement, elementComponent: TextInputComponentType | MathTableComponent): void {
+  open(inputElement: HTMLElement,
+       elementComponent: TextInputComponentType | MathTableComponent | TextAreaMathComponent): void {
     this.addInputAssistanceToKeyboard = elementComponent.elementModel.addInputAssistanceToKeyboard;
     this.preset = elementComponent.elementModel.inputAssistancePreset;
     this.setCurrentKeyInputElement(inputElement, elementComponent);
diff --git a/projects/player/src/app/services/keypad.service.ts b/projects/player/src/app/services/keypad.service.ts
index f278515b7..86220e5ee 100644
--- a/projects/player/src/app/services/keypad.service.ts
+++ b/projects/player/src/app/services/keypad.service.ts
@@ -1,6 +1,7 @@
 import { Injectable } from '@angular/core';
 import { TextInputComponentType } from 'player/src/app/models/text-input-component.type';
 import { MathTableComponent } from 'common/components/input-elements/math-table.component';
+import { TextAreaMathComponent } from 'common/components/input-elements/text-area-math/text-area-math.component';
 import { InputService } from './input-service';
 
 @Injectable({
@@ -10,7 +11,8 @@ export class KeypadService extends InputService {
   position: 'floating' | 'right' = 'floating';
 
   async toggleAsync(focusedTextInput: { inputElement: HTMLElement; focused: boolean },
-                    elementComponent: TextInputComponentType | MathTableComponent): Promise<boolean> {
+                    elementComponent: TextInputComponentType | MathTableComponent | TextAreaMathComponent
+  ): Promise<boolean> {
     this.willToggle.emit(this.isOpen);
     return new Promise(resolve => {
       setTimeout(() => resolve(this.toggle(focusedTextInput, elementComponent)), 100);
@@ -18,7 +20,7 @@ export class KeypadService extends InputService {
   }
 
   private toggle(focusedTextInput: { inputElement: HTMLElement; focused: boolean },
-                 elementComponent: TextInputComponentType | MathTableComponent): boolean {
+                 elementComponent: TextInputComponentType | MathTableComponent | TextAreaMathComponent): boolean {
     if (focusedTextInput.focused) {
       this.open(focusedTextInput.inputElement, elementComponent);
     } else {
@@ -28,7 +30,7 @@ export class KeypadService extends InputService {
   }
 
   open(inputElement: HTMLElement,
-       elementComponent: TextInputComponentType | MathTableComponent):
+       elementComponent: TextInputComponentType | MathTableComponent | TextAreaMathComponent):
     void {
     this.preset = elementComponent.elementModel.inputAssistancePreset;
     this.position = elementComponent.elementModel.inputAssistancePosition;
-- 
GitLab