From 37c16a07ab8e8e7faaf9451f472293e81be21faf Mon Sep 17 00:00:00 2001 From: jojohoch <joachim.hoch@iqb.hu-berlin.de> Date: Tue, 26 Nov 2024 16:28:16 +0100 Subject: [PATCH] [player] Add container for math keyboard --- docs/unit_definition_changelog.txt | 17 ++-- .../input-elements/math-field.component.ts | 14 ++-- .../text-area-math/area-segment.component.ts | 6 +- .../text-area-math.component.ts | 17 ++-- .../text-input-component.directive.ts | 2 +- projects/common/math-editor.module.ts | 17 ++-- .../elements/input-elements/text-area-math.ts | 26 +----- .../services/range-selection-service.ts | 7 +- .../modules/logging/services/log.service.ts | 2 +- projects/player/src/app/app.module.ts | 6 +- .../compound-group-element.component.ts | 4 +- .../element-group-selection.component.ts | 4 +- .../input-group-element.component.html | 5 -- .../input-group-element.component.ts | 2 - .../text-input-group-element.component.html | 7 ++ .../text-input-group-element.component.ts | 4 + .../floating-keypad.component.ts | 2 +- .../player-layout.component.html | 48 +++++------ .../player-layout.component.spec.ts | 15 +++- .../player-layout/player-layout.component.ts | 2 + .../math-keyboard-container.component.html | 3 + .../math-keyboard-container.component.scss | 2 + .../math-keyboard-container.component.ts | 34 ++++++++ .../directives/text-input-group.directive.ts | 80 +++++++++++-------- .../app/models/text-input-component.type.ts | 5 +- .../player/src/app/services/input-service.ts | 5 +- .../src/app/services/keyboard.service.ts | 26 +----- .../services/math-keyboard.service.spec.ts | 16 ++++ .../src/app/services/math-keyboard.service.ts | 31 +++++++ .../app/services/scroll-to-input.service.ts | 27 +++++++ 30 files changed, 276 insertions(+), 160 deletions(-) create mode 100644 projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.html create mode 100644 projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.scss create mode 100644 projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.ts create mode 100644 projects/player/src/app/services/math-keyboard.service.spec.ts create mode 100644 projects/player/src/app/services/math-keyboard.service.ts create mode 100644 projects/player/src/app/services/scroll-to-input.service.ts diff --git a/docs/unit_definition_changelog.txt b/docs/unit_definition_changelog.txt index 042e4f737..666cb4c08 100644 --- a/docs/unit_definition_changelog.txt +++ b/docs/unit_definition_changelog.txt @@ -200,10 +200,13 @@ iqb-aspect-definition@1.0.0 - 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 + - new property:inputAssistancePreset + - new property:inputAssistanceCustomKeys + - new property:inputAssistancePosition + - new property:inputAssistanceFloatingStartPosition; + - new property:restrictedToInputAssistanceChars + - new property:hasArrowKeys + - new property:hasBackspaceKey + - new property:showSoftwareKeyboard + - new property:addInputAssistanceToKeyboard + - new property:hideNativeKeyboard diff --git a/projects/common/components/input-elements/math-field.component.ts b/projects/common/components/input-elements/math-field.component.ts index e9d1c7c50..98d728e90 100644 --- a/projects/common/components/input-elements/math-field.component.ts +++ b/projects/common/components/input-elements/math-field.component.ts @@ -3,8 +3,8 @@ import { Component, Input, Pipe, PipeTransform } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; -import { FormElementComponent } from 'common/directives/form-element-component.directive'; import { MathFieldElement } from 'common/models/elements/input-elements/math-field'; +import { TextInputComponent } from 'common/directives/text-input-component.directive'; @Component({ selector: 'aspect-math-field', @@ -18,10 +18,12 @@ import { MathFieldElement } from 'common/models/elements/input-elements/math-fie [style.background-color]="elementModel.styling.backgroundColor"> <label>{{elementModel.label}}</label><br> <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()"> + [readonly]="elementModel.readOnly" + [enableModeSwitch]="elementModel.enableModeSwitch" + (input)="elementFormControl.setValue($any($event.target).value)" + (focusIn)="focusChanged.emit({ inputElement: $event, focused: true })" + (focusOut)="elementFormControl.markAsTouched(); + focusChanged.emit({ inputElement: $event, focused: true })"> </aspect-math-input> <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched" class="error-message"> @@ -31,7 +33,7 @@ import { MathFieldElement } from 'common/models/elements/input-elements/math-fie `, styles: ['.error-message {font-size: 75%; margin-top: 15px; margin-left: 10px;}'] }) -export class MathFieldComponent extends FormElementComponent { +export class MathFieldComponent extends TextInputComponent { @Input() elementModel!: MathFieldElement; } 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 index e4734a675..ba27b24f0 100644 --- 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 @@ -12,7 +12,8 @@ import { BehaviorSubject } from 'rxjs'; <aspect-math-input #inputComponent [fullWidth]="false" [value]="value" - (focusIn)="selectedFocus.next(this.index)" + (focusIn)="onFocusIn($event)" + (focusOut)="focusOut.emit($event)" (valueChange)="valueChanged.emit({ index: index, value: $event})"> </aspect-math-input> } @else { @@ -50,9 +51,6 @@ export class AreaSegmentComponent { @ViewChild('inputComponent') inputComponent!: AreaTextInputComponent | MathInputComponent; setFocus(offset?: number) { - // if (document.activeElement instanceof HTMLElement) { - // document.activeElement.blur(); - // } this.inputComponent.setFocus(offset); } 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 index 758ea4c31..4c3c86e74 100644 --- 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 @@ -2,16 +2,17 @@ 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'; +import { TextInputComponent } from 'common/directives/text-input-component.directive'; @Component({ selector: 'aspect-text-area-math', template: ` + <label class="label">{{elementModel.label}}</label><br> <button class="insert-formula-button" mat-button cdkOverlayOrigin #trigger="cdkOverlayOrigin" @@ -42,9 +43,9 @@ import { RangeSelectionService } from 'common/services/range-selection-service'; [selectedFocus]="selectedFocus" (valueChanged)="onValueChanged($event)" [index]="i" + (onKeyDown)="onKeyDown.emit($event)" (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> } @@ -54,22 +55,16 @@ import { RangeSelectionService } from 'common/services/range-selection-service'; </mat-error> `, styles: [ + '.label {font-size: 20px; line-height: 135%;}', '.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%;}', + '.text-area {border: 1px solid black; border-radius: 3px; padding: 3px;}', '.insert-formula-button {font-size: large; width: 160px; background-color: #ddd; padding: 15px 10px; height: 55px;}' ] }) -export class TextAreaMathComponent extends FormElementComponent implements OnInit { +export class TextAreaMathComponent extends TextInputComponent 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; diff --git a/projects/common/directives/text-input-component.directive.ts b/projects/common/directives/text-input-component.directive.ts index a2cfa91ee..06755a8c4 100644 --- a/projects/common/directives/text-input-component.directive.ts +++ b/projects/common/directives/text-input-component.directive.ts @@ -8,6 +8,6 @@ export abstract class TextInputComponent extends FormElementComponent implements @Output() focusChanged = new EventEmitter<{ inputElement: HTMLElement; focused: boolean }>(); @Output() onKeyDown = new EventEmitter<{ keyboardEvent: KeyboardEvent; - inputElement: HTMLInputElement | HTMLTextAreaElement + inputElement: HTMLInputElement | HTMLTextAreaElement | HTMLElement; }>(); } diff --git a/projects/common/math-editor.module.ts b/projects/common/math-editor.module.ts index edc3fa49d..9d83c806a 100644 --- a/projects/common/math-editor.module.ts +++ b/projects/common/math-editor.module.ts @@ -21,8 +21,8 @@ import { MatButtonToggleChange, MatButtonToggleModule } from '@angular/material/ [class.full-width]="fullWidth" [class.inline-block]="!fullWidth" [class.read-only]="readonly" - (focusin)="onFocusIn($event)" - (focusout)="onFocusOut($event)"> + (focusin)="onFocusIn()" + (focusout)="onFocusOut()"> </div> `, styles: [` @@ -58,8 +58,8 @@ export class MathInputComponent implements AfterViewInit, OnChanges { @Input() readonly: boolean = false; @Input() enableModeSwitch: boolean = false; @Output() valueChange: EventEmitter<string> = new EventEmitter(); - @Output() focusIn: EventEmitter<FocusEvent> = new EventEmitter(); - @Output() focusOut: EventEmitter<FocusEvent> = new EventEmitter(); + @Output() focusIn: EventEmitter<MathfieldElement> = new EventEmitter(); + @Output() focusOut: EventEmitter<MathfieldElement> = new EventEmitter(); @ViewChild('inputRef') inputRef!: ElementRef; @ViewChild('container') container!: ElementRef; @@ -79,6 +79,7 @@ export class MathInputComponent implements AfterViewInit, OnChanges { this.inputRef.nativeElement.appendChild(this.mathFieldElement); this.mathFieldElement.value = this.value; this.mathFieldElement.readOnly = this.readonly; + setTimeout(() => { this.mathFieldElement.menuItems = []; }); // Disable context menu } ngOnChanges(changes: SimpleChanges): void { @@ -105,14 +106,14 @@ export class MathInputComponent implements AfterViewInit, OnChanges { this.valueChange.emit(this.mathFieldElement.getValue()); } - onFocusIn(event: FocusEvent) { - this.focusIn.emit(event); + onFocusIn() { + this.focusIn.emit(this.mathFieldElement); window.mathVirtualKeyboard.show(); } - onFocusOut(event: FocusEvent) { + onFocusOut() { + this.focusOut.emit(this.mathFieldElement); window.mathVirtualKeyboard.hide(); - this.focusOut.emit(event); } } 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 7f3d3e22c..e244d59d5 100644 --- a/projects/common/models/elements/input-elements/text-area-math.ts +++ b/projects/common/models/elements/input-elements/text-area-math.ts @@ -1,6 +1,4 @@ -import { - InputElement -} from 'common/models/elements/element'; +import { TextInputElement } from 'common/models/elements/element'; import { BasicStyles, PositionProperties, @@ -13,25 +11,16 @@ import { TextAreaMathComponent } from 'common/components/input-elements/text-are import { environment } from 'common/environment'; import { AbstractIDService, - InputAssistancePreset, - InputElementProperties, - KeyInputElementProperties, + KeyInputElementProperties, TextInputElementProperties, UIElementType } from 'common/interfaces'; import { InstantiationEror } from 'common/errors'; -export class TextAreaMathElement extends InputElement implements TextAreaMathProperties { +export class TextAreaMathElement extends TextInputElement implements TextAreaMathProperties { type: UIElementType = 'text-area-math'; 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; @@ -45,13 +34,6 @@ 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 { @@ -91,7 +73,7 @@ export class TextAreaMathElement extends InputElement implements TextAreaMathPro } } -export interface TextAreaMathProperties extends InputElementProperties, KeyInputElementProperties { +export interface TextAreaMathProperties extends TextInputElementProperties, KeyInputElementProperties { rowCount: number; hasAutoHeight: boolean; position: PositionProperties; diff --git a/projects/common/services/range-selection-service.ts b/projects/common/services/range-selection-service.ts index 43d391442..816961d70 100644 --- a/projects/common/services/range-selection-service.ts +++ b/projects/common/services/range-selection-service.ts @@ -51,24 +51,21 @@ export class RangeSelectionService { 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 + return true; } 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 true; } } } diff --git a/projects/player/modules/logging/services/log.service.ts b/projects/player/modules/logging/services/log.service.ts index 47ad6e147..651ce17b3 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 = 4; + static level: LogLevel = 0; static error(...args: unknown[]): void { if (LogService.level >= LogLevel.ERROR) { diff --git a/projects/player/src/app/app.module.ts b/projects/player/src/app/app.module.ts index 3aed1aae1..645d65a63 100644 --- a/projects/player/src/app/app.module.ts +++ b/projects/player/src/app/app.module.ts @@ -67,6 +67,9 @@ import { ExternalAppGroupElementComponent } from import { InputAssistanceCustomKeysPipe } from './pipes/input-assistance-custom-keys.pipe'; import { HasNextPagePipe } from './pipes/has-next-page.pipe'; import { IsValidPagePipe } from './pipes/is-valid-page.pipe'; +import { + MathKeyboardContainerComponent +} from 'player/src/app/components/math-keyboard-container/math-keyboard-container.component'; @NgModule({ declarations: [ @@ -125,7 +128,8 @@ import { IsValidPagePipe } from './pipes/is-valid-page.pipe'; MarkablesContainerComponent, IsEnabledNavigationTargetPipe, MarkingPanelComponent, - UnitNavNextComponent + UnitNavNextComponent, + MathKeyboardContainerComponent ], providers: [ { provide: APIService, useExisting: MetaDataService }, diff --git a/projects/player/src/app/components/elements/compound-group-element/compound-group-element.component.ts b/projects/player/src/app/components/elements/compound-group-element/compound-group-element.component.ts index 6d04165a0..3086e8532 100644 --- a/projects/player/src/app/components/elements/compound-group-element/compound-group-element.component.ts +++ b/projects/player/src/app/components/elements/compound-group-element/compound-group-element.component.ts @@ -45,6 +45,7 @@ import { ValidationService } from '../../../services/validation.service'; import { KeypadService } from '../../../services/keypad.service'; import { KeyboardService } from '../../../services/keyboard.service'; import { DeviceService } from '../../../services/device.service'; +import { MathKeyboardService } from 'player/src/app/services/math-keyboard.service'; @Component({ selector: 'aspect-compound-group-element', @@ -74,6 +75,7 @@ export class CompoundGroupElementComponent extends TextInputGroupDirective imple public keyboardService: KeyboardService, public deviceService: DeviceService, public keypadService: KeypadService, + public mathKeyboardService: MathKeyboardService, public unitStateService: UnitStateService, public elementModelElementCodeMappingService: ElementModelElementCodeMappingService, public veronaSubscriptionService: VeronaSubscriptionService, @@ -330,7 +332,7 @@ export class CompoundGroupElementComponent extends TextInputGroupDirective imple private onKeyDown(event: { keyboardEvent: KeyboardEvent; - inputElement: HTMLInputElement | HTMLTextAreaElement + inputElement: HTMLInputElement | HTMLTextAreaElement | HTMLElement }, elementModel: InputElement): void { this.detectHardwareKeyboard(elementModel); this.checkInputLimitation(event, elementModel); 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 f2e4e1505..1ca7ff0cc 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 @@ -13,13 +13,13 @@ export class ElementGroupSelectionComponent implements OnInit { @Input() pageIndex!: number; groups: ElementGroupInterface[] = [ - { name: 'textInputGroup', types: ['text-field', 'text-area', 'spell-correct', 'text-area-math'] }, + { name: 'textInputGroup', types: ['text-field', 'text-area', 'spell-correct', 'text-area-math', 'math-field'] }, { name: 'mediaPlayerGroup', types: ['audio', 'video'] }, { name: 'inputGroup', types: [ 'checkbox', 'slider', 'drop-list', 'radio', 'radio-group-images', - 'dropdown', 'hotspot-image', 'math-field' + 'dropdown', 'hotspot-image' ] }, { 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 95562c52c..9fb3b161a 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 @@ -35,9 +35,4 @@ [parentForm]="form" [elementModel]="elementModel | cast: DropdownElement"> </aspect-dropdown> - <aspect-math-field *ngIf="elementModel.type === 'math-field'" - #elementComponent - [parentForm]="form" - [elementModel]="elementModel | cast: MathFieldElement"> - </aspect-math-field> </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 53ddef17a..8efde3f17 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 @@ -11,7 +11,6 @@ import { RadioButtonGroupComplexElement } from 'common/models/elements/input-ele 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 { ValidationService } from '../../../services/validation.service'; import { ElementFormGroupDirective } from '../../../directives/element-form-group.directive'; import { ElementModelElementCodeMappingService } from '../../../services/element-model-element-code-mapping.service'; @@ -31,7 +30,6 @@ export class InputGroupElementComponent extends ElementFormGroupDirective implem RadioButtonGroupComplexElement!: RadioButtonGroupComplexElement; DropdownElement!: DropdownElement; HotspotImageElement!: HotspotImageElement; - MathFieldElement!: MathFieldElement; 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 1485b28a8..d8cda8bdc 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 @@ -32,6 +32,13 @@ (onKeyDown)="detectHardwareKeyboard(elementModel)" (focusChanged)="toggleKeyInput($event, elementComponent)"> </aspect-text-area-math> + <aspect-math-field + *ngIf="elementModel.type === 'math-field'" + #elementComponent + [parentForm]="form" + [elementModel]="elementModel | cast: MathFieldElement" + (focusChanged)="toggleKeyInput($event, elementComponent)"> + </aspect-math-field> </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 e877382e9..84cb33d6e 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 @@ -7,8 +7,10 @@ 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 { MathFieldElement } from 'common/models/elements/input-elements/math-field'; import { InputElement } from 'common/models/elements/element'; import { TextInputGroupDirective } from 'player/src/app/directives/text-input-group.directive'; +import { MathKeyboardService } from 'player/src/app/services/math-keyboard.service'; import { DeviceService } from '../../../services/device.service'; import { KeyboardService } from '../../../services/keyboard.service'; import { ValidationService } from '../../../services/validation.service'; @@ -29,10 +31,12 @@ export class TextInputGroupElementComponent TextFieldElement!: TextFieldElement; SpellCorrectElement!: SpellCorrectElement; TextAreaMathElement!: TextAreaMathElement; + MathFieldElement!: MathFieldElement; constructor( public keyboardService: KeyboardService, public keypadService: KeypadService, + public mathKeyboardService: MathKeyboardService, public unitStateService: UnitStateService, public elementModelElementCodeMappingService: ElementModelElementCodeMappingService, public veronaSubscriptionService: VeronaSubscriptionService, diff --git a/projects/player/src/app/components/floating-keypad/floating-keypad.component.ts b/projects/player/src/app/components/floating-keypad/floating-keypad.component.ts index cfd646f89..1779df500 100644 --- a/projects/player/src/app/components/floating-keypad/floating-keypad.component.ts +++ b/projects/player/src/app/components/floating-keypad/floating-keypad.component.ts @@ -56,7 +56,7 @@ export class FloatingKeypadComponent implements OnChanges { ...position, offsetY: this.getOffsetY(this.keypadService.elementComponent.elementModel.type, index > 0) })); - } else { + } else if (startPosition === 'endCenter') { this.overlayPositions = [...FloatingKeypadComponent.overlayPositionsConfig[startPosition]]; } } diff --git a/projects/player/src/app/components/layouts/player-layout/player-layout.component.html b/projects/player/src/app/components/layouts/player-layout/player-layout.component.html index a601a2307..3a2b161e5 100644 --- a/projects/player/src/app/components/layouts/player-layout/player-layout.component.html +++ b/projects/player/src/app/components/layouts/player-layout/player-layout.component.html @@ -4,31 +4,33 @@ <ng-content></ng-content> </div> <aspect-keypad - *ngIf="keypadService.isOpen && keypadService.position === 'right'" - @keypadSlideInOut - [@.disabled] = "isKeypadAnimationDisabled" - [inputElement]="keypadService.inputElement" - [position]="keypadService.position" - [preset]="keypadService.preset" - [customKeys]="keypadService.elementComponent.elementModel.inputAssistanceCustomKeys | inputAssistanceCustomKeys" - [restrictToAllowedKeys]="!!keypadService.elementComponent.elementModel.restrictedToInputAssistanceChars" - [hasArrowKeys]="!!keypadService.elementComponent.elementModel.hasArrowKeys" - [hasBackspaceKey]="!!keypadService.elementComponent.elementModel.hasBackspaceKey" - [hasReturnKey]="keypadService.elementComponent.elementModel | hasReturnKey" - (backSpaceClicked)="keypadService.deleteCharacters.emit(true)" - (keyClicked)="keypadService.enterKey.emit($event)" - (select)="keypadService.select.emit($event)"> + *ngIf="keypadService.isOpen && keypadService.position === 'right'" + @keypadSlideInOut + [@.disabled] = "isKeypadAnimationDisabled" + [inputElement]="keypadService.inputElement" + [position]="keypadService.position" + [preset]="keypadService.preset" + [customKeys]="keypadService.elementComponent.elementModel.inputAssistanceCustomKeys | inputAssistanceCustomKeys" + [restrictToAllowedKeys]="!!keypadService.elementComponent.elementModel.restrictedToInputAssistanceChars" + [hasArrowKeys]="!!keypadService.elementComponent.elementModel.hasArrowKeys" + [hasBackspaceKey]="!!keypadService.elementComponent.elementModel.hasBackspaceKey" + [hasReturnKey]="keypadService.elementComponent.elementModel | hasReturnKey" + (backSpaceClicked)="keypadService.deleteCharacters.emit(true)" + (keyClicked)="keypadService.enterKey.emit($event)" + (select)="keypadService.select.emit($event)"> </aspect-keypad> </div> <aspect-keyboard - *ngIf="keyboardService.isOpen" - @keyboardSlideInOut - [@.disabled] = "isKeyboardAnimationDisabled" - (@keyboardSlideInOut.done)="keyboardService.scrollElement()" - [addInputAssistanceToKeyboard]="keyboardService.addInputAssistanceToKeyboard" - [preset]="keyboardService.preset" - [customKeys]="keyboardService.elementComponent.elementModel.inputAssistanceCustomKeys | inputAssistanceCustomKeys" - (keyClicked)="keyboardService.enterKey.emit($event)" - (backspaceClicked)="keyboardService.deleteCharacters.emit(true)"> + *ngIf="keyboardService.isOpen" + @keyboardSlideInOut + [@.disabled] = "isKeyboardAnimationDisabled" + (@keyboardSlideInOut.done)="keyboardService.scrollElement()" + [addInputAssistanceToKeyboard]="keyboardService.addInputAssistanceToKeyboard" + [preset]="keyboardService.preset" + [customKeys]="keyboardService.elementComponent.elementModel.inputAssistanceCustomKeys | inputAssistanceCustomKeys" + (keyClicked)="keyboardService.enterKey.emit($event)" + (backspaceClicked)="keyboardService.deleteCharacters.emit(true)"> </aspect-keyboard> + <aspect-math-keyboard-container> + </aspect-math-keyboard-container> </div> diff --git a/projects/player/src/app/components/layouts/player-layout/player-layout.component.spec.ts b/projects/player/src/app/components/layouts/player-layout/player-layout.component.spec.ts index b5cfac127..42e8848c3 100644 --- a/projects/player/src/app/components/layouts/player-layout/player-layout.component.spec.ts +++ b/projects/player/src/app/components/layouts/player-layout/player-layout.component.spec.ts @@ -6,11 +6,13 @@ import { Component, Directive, Input } from '@angular/core'; import { Page } from 'common/models/page'; import { APIService } from 'common/shared.module'; import { PagingMode } from 'player/modules/verona/models/verona'; +import { BrowserAnimationsModule, provideAnimations } from '@angular/platform-browser/animations'; import { PlayerLayoutComponent } from './player-layout.component'; describe('PlayerLayoutComponent', () => { let component: PlayerLayoutComponent; let fixture: ComponentFixture<PlayerLayoutComponent>; + @Directive({ selector: '[aspectPlayerState]' }) class PlayerStateStubDirective { @Input() validPages!: Record<string, string>; @@ -33,6 +35,9 @@ describe('PlayerLayoutComponent', () => { @Input() alwaysVisiblePagePosition!: 'top' | 'bottom' | 'left' | 'right'; } + @Component({ selector: 'aspect-math-keyboard-container', template: '' }) + class MockMathKeyboardContainerComponent {} + beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ @@ -40,9 +45,15 @@ describe('PlayerLayoutComponent', () => { PagesLayoutStubComponent, AlwaysVisiblePagePipe, ScrollPagesPipe, - PlayerStateStubDirective + PlayerStateStubDirective, + MockMathKeyboardContainerComponent + ], + imports: [ + BrowserAnimationsModule ], - providers: [{ provide: APIService, useClass: ApiStubService }] + providers: [ + provideAnimations(), + { provide: APIService, useClass: ApiStubService }] }) .compileComponents(); }); 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 32d3e432a..9df6e040d 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 @@ -39,6 +39,8 @@ export class PlayerLayoutComponent implements OnDestroy { private isKeyboardToggling: number = 0; private ngUnsubscribe = new Subject<void>(); + protected readonly window = window; + constructor( public keypadService: KeypadService, public keyboardService: KeyboardService diff --git a/projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.html b/projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.html new file mode 100644 index 000000000..0b0428089 --- /dev/null +++ b/projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.html @@ -0,0 +1,3 @@ +<div #mathKeyboard + [style.height.px]="mathKeyboardService.keyboardHeight" + class="math-keyboard-container"></div> diff --git a/projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.scss b/projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.scss new file mode 100644 index 000000000..139597f9c --- /dev/null +++ b/projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.scss @@ -0,0 +1,2 @@ + + diff --git a/projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.ts b/projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.ts new file mode 100644 index 000000000..ee1e06a55 --- /dev/null +++ b/projects/player/src/app/components/math-keyboard-container/math-keyboard-container.component.ts @@ -0,0 +1,34 @@ +import { + AfterViewInit, Component, ElementRef, OnDestroy, ViewChild +} from '@angular/core'; +import { MathKeyboardService } from 'player/src/app/services/math-keyboard.service'; + +@Component({ + selector: 'aspect-math-keyboard-container', + standalone: true, + imports: [], + templateUrl: './math-keyboard-container.component.html', + styleUrl: './math-keyboard-container.component.scss' +}) +export class MathKeyboardContainerComponent implements AfterViewInit, OnDestroy { + @ViewChild('mathKeyboard') mathKeyboard!: ElementRef; + + constructor(public mathKeyboardService: MathKeyboardService) {} + + ngAfterViewInit(): void { + window.mathVirtualKeyboard.container = this.mathKeyboard.nativeElement; + window.mathVirtualKeyboard.addEventListener('geometrychange', () => this.updateKeyboard()); + } + + updateKeyboard(): void { + this.mathKeyboardService.keyboardHeight = + window.mathVirtualKeyboard.boundingRect.height; + if (this.mathKeyboardService.keyboardHeight) { + setTimeout(() => this.mathKeyboardService.scrollElement()); + } + } + + ngOnDestroy(): void { + window.mathVirtualKeyboard.removeEventListener('geometrychange', () => this.updateKeyboard()); + } +} 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 2c02fed1d..9fd1c36ee 100644 --- a/projects/player/src/app/directives/text-input-group.directive.ts +++ b/projects/player/src/app/directives/text-input-group.directive.ts @@ -8,8 +8,10 @@ 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'; +import { MathfieldElement } from 'mathlive'; +import { MathKeyboardService } from 'player/src/app/services/math-keyboard.service'; +import { MathFieldComponent } from 'common/components/input-elements/math-field.component'; @Directive() export abstract class TextInputGroupDirective extends ElementFormGroupDirective implements OnDestroy { @@ -25,6 +27,7 @@ export abstract class TextInputGroupDirective extends ElementFormGroupDirective abstract deviceService: DeviceService; abstract keypadService: KeypadService; abstract keyboardService: KeyboardService; + abstract mathKeyboardService: MathKeyboardService; private shallOpenKeypad(elementModel: InputElement): boolean { return !!elementModel.inputAssistancePreset && @@ -34,50 +37,59 @@ export abstract class TextInputGroupDirective extends ElementFormGroupDirective } async toggleKeyInput(focusedTextInput: { inputElement: HTMLElement; focused: boolean }, - elementComponent: TextInputComponentType | TextAreaMathComponent): Promise<void> { + elementComponent: TextInputComponentType | MathFieldComponent): Promise<void> { + const isMathInput = focusedTextInput.inputElement instanceof MathfieldElement; const promises: Promise<boolean>[] = []; - if (elementComponent.elementModel.showSoftwareKeyboard && !elementComponent.elementModel.readOnly) { - promises.push(this.keyboardService - .toggleAsync(focusedTextInput, elementComponent, this.deviceService.isMobileWithoutHardwareKeyboard)); - } - if (this.shallOpenKeypad(elementComponent.elementModel)) { - 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.inputElement = this.getInputElement(focusedTextInput.inputElement); - } - }); + if (isMathInput) { + this.mathKeyboardService + .toggle(focusedTextInput as { inputElement: MathfieldElement; focused: boolean }, + elementComponent); + } else if (!(elementComponent instanceof MathFieldComponent)) { + if (elementComponent.elementModel.showSoftwareKeyboard && !elementComponent.elementModel.readOnly) { + promises.push(this.keyboardService + .toggleAsync(focusedTextInput, elementComponent, this.deviceService.isMobileWithoutHardwareKeyboard)); + } + if (this.shallOpenKeypad(elementComponent.elementModel)) { + 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.inputElement = this.getInputElement(focusedTextInput.inputElement); + } + }); + } } } // eslint-disable-next-line class-methods-use-this checkInputLimitation(event: { keyboardEvent: KeyboardEvent; - inputElement: HTMLInputElement | HTMLTextAreaElement + inputElement: HTMLInputElement | HTMLTextAreaElement | HTMLElement; }, elementModel: UIElement): void { + const inputValue = TextInputGroupDirective.getValueOfInput(event.inputElement); if (elementModel.maxLength && elementModel.isLimitedToMaxLength && - event.inputElement.value.length === elementModel.maxLength && + inputValue.length === elementModel.maxLength && !['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp'].includes(event.keyboardEvent.key)) { event.keyboardEvent.preventDefault(); } } detectHardwareKeyboard(elementModel: UIElement): void { + console.log('detectHardwareKeyboard', elementModel); if (elementModel.showSoftwareKeyboard) { this.deviceService.hasHardwareKeyboard = true; this.keyboardService.close(); @@ -228,10 +240,14 @@ export abstract class TextInputGroupDirective extends ElementFormGroupDirective } private getInputElementValue(): string { - if (this.inputElement instanceof HTMLInputElement || this.inputElement instanceof HTMLTextAreaElement) { - return this.inputElement.value; + return TextInputGroupDirective.getValueOfInput(this.inputElement); + } + + private static getValueOfInput(inputElement: HTMLElement | HTMLInputElement | HTMLTextAreaElement): string { + if (inputElement instanceof HTMLInputElement || inputElement instanceof HTMLTextAreaElement) { + return inputElement.value; } - return this.inputElement.textContent || ''; + return inputElement.textContent || ''; } private insert(keyAtPosition: { diff --git a/projects/player/src/app/models/text-input-component.type.ts b/projects/player/src/app/models/text-input-component.type.ts index 58209b816..c11cc285a 100644 --- a/projects/player/src/app/models/text-input-component.type.ts +++ b/projects/player/src/app/models/text-input-component.type.ts @@ -4,6 +4,9 @@ import { SpellCorrectComponent } from 'common/components/input-elements/spell-co import { TextFieldSimpleComponent } from 'common/components/compound-elements/cloze/cloze-child-elements/text-field-simple.component'; +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 { MathFieldComponent } from 'common/components/input-elements/math-field.component'; export type TextInputComponentType = - TextAreaComponent | TextFieldComponent | SpellCorrectComponent | TextFieldSimpleComponent; + TextAreaComponent | TextFieldComponent | SpellCorrectComponent | TextFieldSimpleComponent | TextAreaMathComponent; diff --git a/projects/player/src/app/services/input-service.ts b/projects/player/src/app/services/input-service.ts index 123c0c069..4b412b11d 100644 --- a/projects/player/src/app/services/input-service.ts +++ b/projects/player/src/app/services/input-service.ts @@ -3,13 +3,14 @@ import { TextInputComponentType } from 'player/src/app/models/text-input-compone 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'; +import { MathFieldComponent } from 'common/components/input-elements/math-field.component'; @Injectable({ providedIn: 'root' }) export abstract class InputService { preset: InputAssistancePreset = null; - elementComponent!: TextInputComponentType | MathTableComponent | TextAreaMathComponent; + elementComponent!: TextInputComponentType | MathTableComponent | TextAreaMathComponent | MathFieldComponent; inputElement!: HTMLTextAreaElement | HTMLInputElement | HTMLElement; isOpen: boolean = false; @@ -20,7 +21,7 @@ export abstract class InputService { setCurrentKeyInputElement( focusedElement: HTMLElement, - elementComponent: TextInputComponentType | MathTableComponent | TextAreaMathComponent + elementComponent: TextInputComponentType | MathTableComponent | TextAreaMathComponent | MathFieldComponent ): 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 d48a7e508..54fb8355a 100644 --- a/projects/player/src/app/services/keyboard.service.ts +++ b/projects/player/src/app/services/keyboard.service.ts @@ -2,12 +2,12 @@ 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'; +import { ScrollToInputService } from 'player/src/app/services/scroll-to-input.service'; @Injectable({ providedIn: 'root' }) -export class KeyboardService extends InputService { +export class KeyboardService extends ScrollToInputService { addInputAssistanceToKeyboard: boolean = false; async toggleAsync(focusedTextInput: { inputElement: HTMLElement; focused: boolean }, @@ -37,28 +37,8 @@ export class KeyboardService extends InputService { elementComponent: TextInputComponentType | MathTableComponent | TextAreaMathComponent): void { this.addInputAssistanceToKeyboard = elementComponent.elementModel.addInputAssistanceToKeyboard; this.preset = elementComponent.elementModel.inputAssistancePreset; + this.keyboardHeight = this.addInputAssistanceToKeyboard ? 380 : 280; this.setCurrentKeyInputElement(inputElement, elementComponent); this.isOpen = true; } - - scrollElement(): void { - if (this.isOpen && this.isElementHiddenByKeyboard()) { - const scrollPositionTarget = this.isViewHighEnoughToCenterElement() ? 'center' : 'start'; - this.elementComponent.domElement.scrollIntoView({ block: scrollPositionTarget }); - } - } - - private isViewHighEnoughToCenterElement(): boolean { - return window.innerHeight - this.getKeyboardHeight() > - this.elementComponent.domElement.getBoundingClientRect().height; - } - - private isElementHiddenByKeyboard(): boolean { - return window.innerHeight - this.elementComponent.domElement.getBoundingClientRect().bottom < - this.getKeyboardHeight(); - } - - private getKeyboardHeight(): number { - return this.addInputAssistanceToKeyboard ? 380 : 280; - } } diff --git a/projects/player/src/app/services/math-keyboard.service.spec.ts b/projects/player/src/app/services/math-keyboard.service.spec.ts new file mode 100644 index 000000000..c4101a6ca --- /dev/null +++ b/projects/player/src/app/services/math-keyboard.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { MathKeyboardService } from './math-keyboard.service'; + +describe('MathKeyboardService', () => { + let service: MathKeyboardService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MathKeyboardService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/projects/player/src/app/services/math-keyboard.service.ts b/projects/player/src/app/services/math-keyboard.service.ts new file mode 100644 index 000000000..b94e24c91 --- /dev/null +++ b/projects/player/src/app/services/math-keyboard.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { MathfieldElement } from 'mathlive'; +import { TextAreaMathComponent } from 'common/components/input-elements/text-area-math/text-area-math.component'; +import { InputService } from 'player/src/app/services/input-service'; +import { ScrollToInputService } from 'player/src/app/services/scroll-to-input.service'; +import { TextInputComponentType } from 'player/src/app/models/text-input-component.type'; +import { MathFieldComponent } from 'common/components/input-elements/math-field.component'; +import { MathTableComponent } from 'common/components/input-elements/math-table.component'; + +@Injectable({ + providedIn: 'root' +}) +export class MathKeyboardService extends ScrollToInputService { + isOpen: boolean = false; + + toggle(focusedTextInput: { inputElement: MathfieldElement; focused: boolean }, + elementComponent: TextInputComponentType | MathFieldComponent): boolean { + if (focusedTextInput.focused) { + console.log('focusedTextInput.focused', focusedTextInput.inputElement); + this.open(focusedTextInput.inputElement, elementComponent); + } else { + this.close(); + } + return this.isOpen; + } + + private open(inputElement: MathfieldElement, elementComponent: TextInputComponentType | MathFieldComponent): void { + this.setCurrentKeyInputElement(inputElement, elementComponent); + this.isOpen = true; + } +} diff --git a/projects/player/src/app/services/scroll-to-input.service.ts b/projects/player/src/app/services/scroll-to-input.service.ts new file mode 100644 index 000000000..d483e4b8b --- /dev/null +++ b/projects/player/src/app/services/scroll-to-input.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { InputService } from 'player/src/app/services/input-service'; + +@Injectable({ + providedIn: 'root' +}) +export abstract class ScrollToInputService extends InputService { + keyboardHeight: number = 0; + + scrollElement(): void { + if (this.isOpen && this.isElementHiddenByKeyboard()) { + const scrollPositionTarget = this.isViewHighEnoughToCenterElement() ? 'center' : 'start'; + console.log('scrollPositionTarget', scrollPositionTarget); + this.elementComponent.domElement.scrollIntoView({ block: scrollPositionTarget }); + } + } + + private isViewHighEnoughToCenterElement(): boolean { + return window.innerHeight - this.keyboardHeight > + this.elementComponent.domElement.getBoundingClientRect().height; + } + + private isElementHiddenByKeyboard(): boolean { + return window.innerHeight - this.elementComponent.domElement.getBoundingClientRect().bottom < + this.keyboardHeight; + } +} -- GitLab