From bc4fa40abd582f929089a9dd22a7d1dd62f8815e Mon Sep 17 00:00:00 2001
From: jojohoch <joachim.hoch@iqb.hu-berlin.de>
Date: Fri, 4 Mar 2022 14:19:29 +0100
Subject: [PATCH] [player] Implement virtual keyboard for text inputs of cloze
 component

---
 .../text-field-simple.component.ts            | 11 +++--
 .../directives/element-component.directive.ts |  2 +-
 projects/common/interfaces/elements.ts        |  2 +
 projects/common/util/element.factory.ts       |  3 ++
 .../element-compound-group.component.html     | 15 ++++++-
 .../element-compound-group.component.ts       | 26 +++++++++++-
 .../element-text-input-group.component.html   | 40 +++++++++----------
 7 files changed, 72 insertions(+), 27 deletions(-)

diff --git a/projects/common/components/ui-elements/text-field-simple.component.ts b/projects/common/components/ui-elements/text-field-simple.component.ts
index 8651ec386..6cd951dbb 100644
--- a/projects/common/components/ui-elements/text-field-simple.component.ts
+++ b/projects/common/components/ui-elements/text-field-simple.component.ts
@@ -1,11 +1,13 @@
-import { Component, Input } from '@angular/core';
+import {
+  Component, EventEmitter, Input, Output
+} from '@angular/core';
 import { FormElementComponent } from '../../directives/form-element-component.directive';
 import { TextFieldSimpleElement } from '../../interfaces/elements';
 
 @Component({
   selector: 'aspect-text-field-simple',
   template: `
-    <input type="text" form="parentForm"
+    <input #input type="text"
            autocomplete="off"
            autocapitalize="none"
            autocorrect="off"
@@ -21,7 +23,9 @@ import { TextFieldSimpleElement } from '../../interfaces/elements';
            [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''"
            [readonly]="elementModel.readOnly"
            [formControl]="elementFormControl"
-           [value]="elementModel.value">
+           [value]="elementModel.value"
+           (focus)="elementModel.inputAssistancePreset !== 'none' ? onFocusChanged.emit(input) : null"
+           (blur)="elementModel.inputAssistancePreset !== 'none' ? onFocusChanged.emit(null): null">
   `,
   styles: [
     'input {border: 1px solid rgba(0,0,0,.12); border-radius: 5px}'
@@ -29,4 +33,5 @@ import { TextFieldSimpleElement } from '../../interfaces/elements';
 })
 export class TextFieldSimpleComponent extends FormElementComponent {
   @Input() elementModel!: TextFieldSimpleElement;
+  @Output() onFocusChanged = new EventEmitter<HTMLElement | null>();
 }
diff --git a/projects/common/directives/element-component.directive.ts b/projects/common/directives/element-component.directive.ts
index 08d18bed0..0993eaf11 100644
--- a/projects/common/directives/element-component.directive.ts
+++ b/projects/common/directives/element-component.directive.ts
@@ -9,7 +9,7 @@ export abstract class ElementComponent implements AfterContentChecked {
   abstract elementModel: UIElement;
   project!: 'player' | 'editor';
 
-  constructor(private elementRef: ElementRef) {}
+  constructor(public elementRef: ElementRef) {}
 
   get domElement(): Element {
     return this.elementRef.nativeElement;
diff --git a/projects/common/interfaces/elements.ts b/projects/common/interfaces/elements.ts
index c5260db34..f706e3364 100644
--- a/projects/common/interfaces/elements.ts
+++ b/projects/common/interfaces/elements.ts
@@ -284,6 +284,8 @@ export interface TextAreaElement extends InputElement {
 
 export interface TextFieldSimpleElement extends InputElement {
   type: 'text-field';
+  inputAssistancePreset: InputAssistancePreset;
+  inputAssistancePosition: 'floating' | 'right';
   styling: BasicStyles; // TODO okay? bg-color?
 }
 
diff --git a/projects/common/util/element.factory.ts b/projects/common/util/element.factory.ts
index e220dd501..762ca1cd8 100644
--- a/projects/common/util/element.factory.ts
+++ b/projects/common/util/element.factory.ts
@@ -467,6 +467,9 @@ export abstract class ElementFactory {
       ...ElementFactory.initInputElement({ height: 25, ...element }),
       type: 'text-field',
       label: element.label !== undefined ? element.label : undefined,
+      inputAssistancePreset: element.inputAssistancePreset !== undefined ? element.inputAssistancePreset : 'none',
+      inputAssistancePosition: element.inputAssistancePosition !== undefined ?
+        element.inputAssistancePosition : 'floating',
       styling: ElementFactory.initBasicStyles(element.styling)
     };
   }
diff --git a/projects/player/src/app/components/element-compound-group/element-compound-group.component.html b/projects/player/src/app/components/element-compound-group/element-compound-group.component.html
index 3431f8717..7c8b896eb 100644
--- a/projects/player/src/app/components/element-compound-group/element-compound-group.component.html
+++ b/projects/player/src/app/components/element-compound-group/element-compound-group.component.html
@@ -1,4 +1,5 @@
-<form [formGroup]="form">
+<form class="inline-container"
+      [formGroup]="form">
   <aspect-cloze
       *ngIf="elementModel.type === 'cloze'"
       #elementComponent
@@ -14,3 +15,15 @@
       (childrenAdded)="onChildrenAdded($event)">
   </aspect-likert>
 </form>
+
+<aspect-floating-keyboard
+    *ngIf="keyboardService.preset !== 'none'"
+    [isKeyboardOpen]="isKeyboardOpen && keyboardService.position === 'floating'"
+    [overlayOrigin]="keyboardService.elementComponent"
+    [inputElement]="keyboardService.inputElement"
+    [position]="keyboardService.position"
+    [preset]="keyboardService.preset"
+    [positionOffset]="0"
+    (deleteCharacter)="keyboardService.deleterCharacters()"
+    (enterKey)="keyboardService.enterKey($event)">
+</aspect-floating-keyboard>
diff --git a/projects/player/src/app/components/element-compound-group/element-compound-group.component.ts b/projects/player/src/app/components/element-compound-group/element-compound-group.component.ts
index b5f07265c..8a24bbb02 100644
--- a/projects/player/src/app/components/element-compound-group/element-compound-group.component.ts
+++ b/projects/player/src/app/components/element-compound-group/element-compound-group.component.ts
@@ -1,7 +1,7 @@
 import { Component, OnInit, ViewChild } from '@angular/core';
 import { TranslateService } from '@ngx-translate/core';
 import {
-  ClozeElement, InputElement, LikertElement
+  ClozeElement, InputElement, LikertElement, TextFieldElement
 } from '../../../../../common/interfaces/elements';
 import { ClozeUtils } from '../../../../../common/util/cloze';
 import { UnitStateService } from '../../services/unit-state.service';
@@ -11,6 +11,10 @@ import { ElementFormGroupDirective } from '../../directives/element-form-group.d
 import { MessageService } from '../../../../../common/services/message.service';
 import { VeronaSubscriptionService } from '../../services/verona-subscription.service';
 import { ValidatorService } from '../../services/validator.service';
+import { KeyboardService } from '../../services/keyboard.service';
+import { TextAreaComponent } from '../../../../../common/components/ui-elements/text-area.component';
+import { TextFieldComponent } from '../../../../../common/components/ui-elements/text-field.component';
+import { TextFieldSimpleComponent } from '../../../../../common/components/ui-elements/text-field-simple.component';
 
 @Component({
   selector: 'aspect-element-compound-group',
@@ -19,10 +23,12 @@ import { ValidatorService } from '../../services/validator.service';
 })
 export class ElementCompoundGroupComponent extends ElementFormGroupDirective implements OnInit {
   @ViewChild('elementComponent') elementComponent!: ElementComponent;
+  isKeyboardOpen!: boolean;
   ClozeElement!: ClozeElement;
   LikertElement!: LikertElement;
 
   constructor(
+    public keyboardService: KeyboardService,
     public unitStateService: UnitStateService,
     public unitStateElementMapperService: UnitStateElementMapperService,
     public translateService: TranslateService,
@@ -44,6 +50,24 @@ export class ElementCompoundGroupComponent extends ElementFormGroupDirective imp
     children.forEach(child => {
       const childModel = child.elementModel as InputElement;
       this.registerAtUnitStateService(childModel.id, childModel.value, child, this.pageIndex);
+      if (childModel.type === 'text-field') {
+        (child as TextFieldSimpleComponent)
+          .onFocusChanged.subscribe(element => this.onFocusChanged(element, child as TextFieldComponent));
+      }
     });
   }
+
+  onFocusChanged(focussedElement: HTMLElement | null, elementComponent: TextAreaComponent | TextFieldComponent): void {
+    if (focussedElement) {
+      const focussedInputElement = this.elementModel.type === 'text-area' ?
+        focussedElement as HTMLTextAreaElement :
+        focussedElement as HTMLInputElement;
+      const preset = (elementComponent.elementModel as TextFieldElement).inputAssistancePreset;
+      const position = (elementComponent.elementModel as TextFieldElement).inputAssistancePosition;
+      this.isKeyboardOpen = this.keyboardService
+        .openKeyboard(focussedInputElement, preset, position, elementComponent);
+    } else {
+      this.isKeyboardOpen = this.keyboardService.closeKeyboard();
+    }
+  }
 }
diff --git a/projects/player/src/app/components/element-text-input-group/element-text-input-group.component.html b/projects/player/src/app/components/element-text-input-group/element-text-input-group.component.html
index 8e451a2b5..f79fb974f 100644
--- a/projects/player/src/app/components/element-text-input-group/element-text-input-group.component.html
+++ b/projects/player/src/app/components/element-text-input-group/element-text-input-group.component.html
@@ -1,27 +1,25 @@
-<div class="inline-container" cdkOverlayOrigin #overlayOrigin="cdkOverlayOrigin">
-  <form [formGroup]="form">
-    <aspect-text-area
-        *ngIf="elementModel.type === 'text-area'"
-        #elementComponent
-        [parentForm]="form"
-        [elementModel]="elementModel | cast: TextAreaElement"
-        (onFocusChanged)="onFocusChanged($event, elementComponent)">
-    </aspect-text-area>
-    <aspect-text-field
-        *ngIf="elementModel.type === 'text-field'"
-        #elementComponent
-        [parentForm]="form"
-        [elementModel]="elementModel | cast: TextFieldElement"
-        (onFocusChanged)="onFocusChanged($event, elementComponent)">
-    </aspect-text-field>
-  </form>
-</div>
+<form class="inline-container"
+      [formGroup]="form">
+  <aspect-text-area
+      *ngIf="elementModel.type === 'text-area'"
+      #elementComponent
+      [parentForm]="form"
+      [elementModel]="elementModel | cast: TextAreaElement"
+      (onFocusChanged)="onFocusChanged($event, elementComponent)">
+  </aspect-text-area>
+  <aspect-text-field
+      *ngIf="elementModel.type === 'text-field'"
+      #elementComponent
+      [parentForm]="form"
+      [elementModel]="elementModel | cast: TextFieldElement"
+      (onFocusChanged)="onFocusChanged($event, elementComponent)">
+  </aspect-text-field>
+</form>
 
 <aspect-floating-keyboard
-    *ngIf="keyboardService.preset !== 'none' &&
-    (elementModel.type === 'text-area' || elementModel.type === 'text-field')"
+    *ngIf="keyboardService.preset !== 'none'"
     [isKeyboardOpen]="isKeyboardOpen && keyboardService.position === 'floating'"
-    [overlayOrigin]="overlayOrigin"
+    [overlayOrigin]="elementComponent"
     [inputElement]="keyboardService.inputElement"
     [position]="keyboardService.position"
     [preset]="keyboardService.preset"
-- 
GitLab