From 87effdf72bfd8367583f62fad67c4e3a36d7465c Mon Sep 17 00:00:00 2001
From: rhenck <richard.henck@iqb.hu-berlin.de>
Date: Wed, 19 Jan 2022 18:18:01 +0100
Subject: [PATCH] Rework compound child components

Compound elements are now supposed to use overlays for their child
elements. This overlay makes selecting child components (by clicking on
them) and marking them as selected (done by the selection service)
possible.
The SelectionService no longer needs special logic to handle compound
children selection, as they now also have an overlay with the same
interface as normal (canvas) elements.

A few modifications in connected directives are necessary. The now
handle children components in a proper array instead of QueryList.

Likert elements do not have clickable children yet and work a little
differently. This should probably be unitized in the future.
---
 .../compound-child-overlay.component.ts       | 51 +++++++++++++++++++
 .../directives/compound-element.directive.ts  | 10 ++--
 projects/common/shared.module.ts              |  4 +-
 .../ui-elements/cloze/cloze.component.ts      | 42 +++++----------
 .../ui-elements/likert/likert.component.ts    |  5 ++
 .../canvas/overlays/canvas-element-overlay.ts | 11 ++--
 .../src/app/services/selection.service.ts     | 28 ----------
 .../element-container.component.ts            |  2 +-
 8 files changed, 83 insertions(+), 70 deletions(-)
 create mode 100644 projects/common/directives/cloze-child-overlay/compound-child-overlay.component.ts

diff --git a/projects/common/directives/cloze-child-overlay/compound-child-overlay.component.ts b/projects/common/directives/cloze-child-overlay/compound-child-overlay.component.ts
new file mode 100644
index 000000000..818d78a32
--- /dev/null
+++ b/projects/common/directives/cloze-child-overlay/compound-child-overlay.component.ts
@@ -0,0 +1,51 @@
+import {
+  Component, EventEmitter, Input, Output, ViewChild
+} from '@angular/core';
+import { FormGroup } from '@angular/forms';
+import { ValueChangeElement } from '../../models/uI-element';
+import { ToggleButtonElement } from '../../ui-elements/toggle-button/toggle-button';
+import { TextFieldSimpleElement } from '../../ui-elements/textfield-simple/text-field-simple-element';
+import { DropListSimpleElement } from '../../ui-elements/drop-list-simple/drop-list-simple';
+import { ElementComponent } from '../element-component.directive';
+
+@Component({
+  selector: 'app-compound-child-overlay',
+  template: `
+    <div [style.outline]="isSelected ? 'purple solid 1px' : ''"
+         (click)="elementSelected.emit(this); $event.stopPropagation();">
+      <app-toggle-button *ngIf="element.type === 'toggle-button'" #childComponent
+                         [parentForm]="parentForm"
+                         [style.display]="'inline-block'"
+                         [style.vertical-align]="'middle'"
+                         [elementModel]="$any(element)"
+                         (elementValueChanged)="elementValueChanged.emit($event)">
+      </app-toggle-button>
+      <app-text-field-simple *ngIf="element.type === 'text-field'" #childComponent
+                             [parentForm]="parentForm"
+                             [style.display]="'inline-block'"
+                             [elementModel]="$any(element)"
+                             (elementValueChanged)="elementValueChanged.emit($event)">
+      </app-text-field-simple>
+      <app-drop-list-simple *ngIf="element.type === 'drop-list'" #childComponent
+                            [parentForm]="parentForm"
+                            [style.display]="'inline-block'"
+                            [style.vertical-align]="'middle'"
+                            [elementModel]="$any(element)"
+                            (elementValueChanged)="elementValueChanged.emit($event)">
+      </app-drop-list-simple>
+    </div>
+  `
+})
+export class CompoundChildOverlayComponent {
+  @Input() element!: ToggleButtonElement | TextFieldSimpleElement | DropListSimpleElement;
+  @Input() parentForm!: FormGroup;
+  @Output() elementValueChanged = new EventEmitter<ValueChangeElement>();
+  @Output() elementSelected = new EventEmitter<CompoundChildOverlayComponent>();
+  @ViewChild('childComponent') childComponent!: ElementComponent;
+
+  isSelected: boolean = false;
+
+  setSelected(newValue: boolean): void {
+    this.isSelected = newValue;
+  }
+}
diff --git a/projects/common/directives/compound-element.directive.ts b/projects/common/directives/compound-element.directive.ts
index 78329ce7f..0d9c30302 100644
--- a/projects/common/directives/compound-element.directive.ts
+++ b/projects/common/directives/compound-element.directive.ts
@@ -5,19 +5,21 @@ import {
 import { FormGroup } from '@angular/forms';
 import { ElementComponent } from './element-component.directive';
 import { InputElement, ValueChangeElement } from '../models/uI-element';
+import { CompoundChildOverlayComponent } from './cloze-child-overlay/compound-child-overlay.component';
+import { LikertRadioButtonGroupComponent } from '../ui-elements/likert/likert-radio-button-group.component';
 
 @Directive({ selector: 'app-compound-element' })
 
 export abstract class CompoundElementComponent extends ElementComponent implements AfterViewInit {
-  @Output() childrenAdded = new EventEmitter<QueryList<ElementComponent>>();
+  @Output() childrenAdded = new EventEmitter<ElementComponent[]>();
   @Output() elementValueChanged = new EventEmitter<ValueChangeElement>();
-  compoundChildren!: QueryList<ElementComponent>;
+  compoundChildren!: QueryList<CompoundChildOverlayComponent | LikertRadioButtonGroupComponent>;
   parentForm!: FormGroup;
-  allowClickThrough = true;
 
   ngAfterViewInit(): void {
-    this.childrenAdded.emit(this.compoundChildren);
+    this.childrenAdded.emit(this.getFormElementChildrenComponents());
   }
 
   abstract getFormElementModelChildren(): InputElement[];
+  abstract getFormElementChildrenComponents(): ElementComponent[];
 }
diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts
index 20f245335..de28c462d 100644
--- a/projects/common/shared.module.ts
+++ b/projects/common/shared.module.ts
@@ -52,6 +52,7 @@ import { ToggleButtonComponent } from './ui-elements/toggle-button/toggle-button
 import { MarkingBarComponent } from './components/marking-bar/marking-bar.component';
 import { StyleMarksPipe } from './ui-elements/cloze/styleMarks.pipe';
 import { MarkingButtonComponent } from './components/marking-bar/marking-button.component';
+import { CompoundChildOverlayComponent } from './directives/cloze-child-overlay/compound-child-overlay.component';
 
 @NgModule({
   imports: [
@@ -105,7 +106,8 @@ import { MarkingButtonComponent } from './components/marking-bar/marking-button.
     ToggleButtonComponent,
     MarkingBarComponent,
     StyleMarksPipe,
-    MarkingButtonComponent
+    MarkingButtonComponent,
+    CompoundChildOverlayComponent
   ],
   exports: [
     CommonModule,
diff --git a/projects/common/ui-elements/cloze/cloze.component.ts b/projects/common/ui-elements/cloze/cloze.component.ts
index 24a129c41..ff4c55755 100644
--- a/projects/common/ui-elements/cloze/cloze.component.ts
+++ b/projects/common/ui-elements/cloze/cloze.component.ts
@@ -5,6 +5,9 @@ import { ClozeElement } from './cloze-element';
 import { CompoundElementComponent } from '../../directives/compound-element.directive';
 import { ClozeDocumentParagraph, ClozeDocumentPart, InputElement } from '../../models/uI-element';
 import { FormElementComponent } from '../../directives/form-element-component.directive';
+import { CompoundChildOverlayComponent } from '../../directives/cloze-child-overlay/compound-child-overlay.component';
+import { ElementComponent } from '../../directives/element-component.directive';
+import { LikertRadioButtonGroupComponent } from '../likert/likert-radio-button-group.component';
 
 @Component({
   selector: 'app-cloze',
@@ -148,32 +151,14 @@ import { FormElementComponent } from '../../directives/form-element-component.di
                [style.height]="'1em'"
                [style.vertical-align]="'middle'">
         </ng-container>
-        <span *ngIf="['ToggleButton', 'DropList', 'TextField'].includes(subPart.type)"
-              (click)="selectElement($any(subPart.attrs).model, $event)">
-                <app-toggle-button *ngIf="subPart.type === 'ToggleButton'" #radioComponent
-                                   [parentForm]="parentForm"
-                                   [style.display]="'inline-block'"
-                                   [style.vertical-align]="'middle'"
-                                   [style.pointerEvents]="allowClickThrough ? 'auto' : 'none'"
-                                   [elementModel]="$any(subPart.attrs).model"
-                                   (elementValueChanged)="elementValueChanged.emit($event)">
-                </app-toggle-button>
-                <app-text-field-simple *ngIf="subPart.type === 'TextField'" #textfieldComponent
-                                       [parentForm]="parentForm"
-                                       [style.display]="'inline-block'"
-                                       [style.pointerEvents]="allowClickThrough ? 'auto' : 'none'"
-                                       [elementModel]="$any(subPart.attrs).model"
-                                       (elementValueChanged)="elementValueChanged.emit($event)">
-                </app-text-field-simple>
-                <app-drop-list-simple *ngIf="subPart.type === 'DropList'" #droplistComponent
+        <span *ngIf="['ToggleButton', 'DropList', 'TextField'].includes(subPart.type)">
+          <app-compound-child-overlay [style.display]="'inline-block'"
                                       [parentForm]="parentForm"
-                                      [style.display]="'inline-block'"
-                                      [style.vertical-align]="'middle'"
-                                      [style.pointerEvents]="allowClickThrough ? 'auto' : 'none'"
-                                      [elementModel]="$any(subPart.attrs).model"
+                                      [element]="$any(subPart).attrs.model"
+                                      (elementSelected)="childElementSelected.emit($event)"
                                       (elementValueChanged)="elementValueChanged.emit($event)">
-                </app-drop-list-simple>
-            </span>
+          </app-compound-child-overlay>
+        </span>
       </ng-container>
     </ng-template>
   `,
@@ -190,9 +175,8 @@ import { FormElementComponent } from '../../directives/form-element-component.di
 })
 export class ClozeComponent extends CompoundElementComponent {
   @Input() elementModel!: ClozeElement;
-  @Output() elementSelected = new EventEmitter<{ element: ClozeElement, event: MouseEvent }>();
-  @ViewChildren('drowdownComponent, textfieldComponent, droplistComponent, radioComponent')
-  compoundChildren!: QueryList<FormElementComponent>;
+  @Output() childElementSelected = new EventEmitter<CompoundChildOverlayComponent>();
+  @ViewChildren(CompoundChildOverlayComponent) compoundChildren!: QueryList<CompoundChildOverlayComponent>;
 
   getFormElementModelChildren(): InputElement[] {
     return this.elementModel.document.content
@@ -203,7 +187,7 @@ export class ClozeComponent extends CompoundElementComponent {
         .concat(currentValue.map((node: ClozeDocumentPart) => node.attrs?.model)), []); // model is in node.attrs.model
   }
 
-  selectElement(element: ClozeElement, event: MouseEvent): void {
-    this.elementSelected.emit({ element: element, event: event });
+  getFormElementChildrenComponents(): ElementComponent[] {
+    return this.compoundChildren.map((child: CompoundChildOverlayComponent) => child.childComponent);
   }
 }
diff --git a/projects/common/ui-elements/likert/likert.component.ts b/projects/common/ui-elements/likert/likert.component.ts
index 0790dfae9..cbc666f0e 100644
--- a/projects/common/ui-elements/likert/likert.component.ts
+++ b/projects/common/ui-elements/likert/likert.component.ts
@@ -5,6 +5,7 @@ import { LikertElement } from './likert-element';
 import { LikertElementRow } from './likert-element-row';
 import { LikertRadioButtonGroupComponent } from './likert-radio-button-group.component';
 import { CompoundElementComponent } from '../../directives/compound-element.directive';
+import { ElementComponent } from '../../directives/element-component.directive';
 
 @Component({
   selector: 'app-likert',
@@ -73,4 +74,8 @@ export class LikertComponent extends CompoundElementComponent {
   getFormElementModelChildren(): LikertElementRow[] {
     return this.elementModel.rows;
   }
+
+  getFormElementChildrenComponents(): ElementComponent[] {
+    return this.compoundChildren.toArray();
+  }
 }
diff --git a/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/canvas-element-overlay.ts b/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/canvas-element-overlay.ts
index 4bfa3ab30..56c4ef4c3 100644
--- a/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/canvas-element-overlay.ts
+++ b/projects/editor/src/app/components/unit-view/page-view/canvas/overlays/canvas-element-overlay.ts
@@ -14,6 +14,7 @@ import { CompoundElementComponent } from
   '../../../../../../../../common/directives/compound-element.directive';
 import { ClozeComponent } from '../../../../../../../../common/ui-elements/cloze/cloze.component';
 import { ClozeElement } from '../../../../../../../../common/ui-elements/cloze/cloze-element';
+import { CompoundChildOverlayComponent } from '../../../../../../../../common/directives/cloze-child-overlay/compound-child-overlay.component';
 
 @Directive()
 export abstract class CanvasElementOverlay implements OnInit, OnDestroy {
@@ -43,14 +44,10 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy {
 
     if (this.childComponent.instance instanceof ClozeComponent) {
       this.childComponent.location.nativeElement.style.pointerEvents = 'unset';
-      (this.childComponent.instance as unknown as ClozeComponent).allowClickThrough = false;
-      this.childComponent.instance.elementSelected
+      this.childComponent.instance.childElementSelected
         .pipe(takeUntil(this.ngUnsubscribe))
-        .subscribe((elementSelectionEvent: { element: ClozeElement, event: MouseEvent }) => {
-          this.selectionService.selectCompoundChild(
-            elementSelectionEvent.element, elementSelectionEvent.event.target as HTMLElement
-          );
-          elementSelectionEvent.event.stopPropagation();
+        .subscribe((elementSelectionEvent: CompoundChildOverlayComponent) => {
+          this.selectionService.selectElement({ elementComponent: elementSelectionEvent, multiSelect: false });
         });
     }
   }
diff --git a/projects/editor/src/app/services/selection.service.ts b/projects/editor/src/app/services/selection.service.ts
index 6c01eead3..8f97598ad 100644
--- a/projects/editor/src/app/services/selection.service.ts
+++ b/projects/editor/src/app/services/selection.service.ts
@@ -28,7 +28,6 @@ export class SelectionService {
     if (!event.multiSelect) {
       this.clearElementSelection();
     }
-    this.removeCompoundChildSelection();
     this.selectedElementComponents.push(event.elementComponent);
     event.elementComponent.setSelected(true);
     this._selectedElements.next(this.selectedElementComponents.map(componentElement => componentElement.element));
@@ -41,31 +40,4 @@ export class SelectionService {
     this.selectedElementComponents = [];
     this._selectedElements.next([]);
   }
-
-  selectCompoundChild(element: UIElement, nativeElement: HTMLElement): void {
-    this.removeCompoundChildSelection();
-
-    this.setCompoundChildSelection(element, nativeElement);
-    this.selectedCompoundChild = { element: element, nativeElement: nativeElement };
-    this._selectedElements.next([element]);
-  }
-
-  private setCompoundChildSelection(element: UIElement, nativeElement: HTMLElement): void {
-    if (element.type === 'text-field') {
-      (nativeElement.children[0] as HTMLElement).style.outline = '1px solid';
-    } else {
-      nativeElement.style.outline = '1px solid';
-    }
-  }
-
-  private removeCompoundChildSelection(): void {
-    if (this.selectedCompoundChild) {
-      if (this.selectedCompoundChild.element.type === 'text-field') {
-        (this.selectedCompoundChild.nativeElement.children[0] as HTMLElement).style.outline = 'unset';
-      } else {
-        this.selectedCompoundChild.nativeElement.style.outline = 'unset';
-      }
-      this.selectedCompoundChild = null;
-    }
-  }
 }
diff --git a/projects/player/src/app/components/element-container/element-container.component.ts b/projects/player/src/app/components/element-container/element-container.component.ts
index 7cbe4c310..86751c2cc 100644
--- a/projects/player/src/app/components/element-container/element-container.component.ts
+++ b/projects/player/src/app/components/element-container/element-container.component.ts
@@ -149,7 +149,7 @@ export class ElementContainerComponent implements OnInit {
     if (elementComponent.childrenAdded) {
       elementComponent.childrenAdded
         .pipe(takeUntil(this.ngUnsubscribe))
-        .subscribe((children: QueryList<ElementComponent>) => {
+        .subscribe((children: ElementComponent[]) => {
           children.forEach((child, index) => {
             const childModel = compoundChildren[index];
             child.elementModel = this.unitStateElementMapperService
-- 
GitLab