From 090cd7c7d43c8416d3ae7fb9fff6ff0d6f54df29 Mon Sep 17 00:00:00 2001
From: jojohoch <joachim.hoch@iqb.hu-berlin.de>
Date: Mon, 29 Jan 2024 14:39:51 +0100
Subject: [PATCH] Add new trigger element

#608
---
 .../components/trigger/trigger.component.ts   | 41 ++++++++++++++
 projects/common/models/elements/element.ts    |  2 +-
 .../common/models/elements/trigger/trigger.ts | 55 +++++++++++++++++++
 projects/common/shared.module.ts              |  3 +
 projects/common/util/element.factory.ts       |  2 +
 .../ui-element-toolbox.component.html         |  5 ++
 .../element-model-properties.component.html   |  8 +++
 .../action-properties.component.ts            |  3 +-
 .../button-properties.component.ts            |  7 +--
 .../editor/src/app/services/id.service.ts     |  3 +-
 projects/editor/src/assets/i18n/de.json       |  3 +-
 .../modules/logging/services/log.service.ts   |  2 +-
 .../element-group-selection.component.ts      |  2 +-
 .../interactive-group-element.component.html  |  9 +++
 .../interactive-group-element.component.ts    | 13 ++++-
 .../directives/in-view-detection.directive.ts |  2 +-
 16 files changed, 145 insertions(+), 15 deletions(-)
 create mode 100644 projects/common/components/trigger/trigger.component.ts
 create mode 100644 projects/common/models/elements/trigger/trigger.ts

diff --git a/projects/common/components/trigger/trigger.component.ts b/projects/common/components/trigger/trigger.component.ts
new file mode 100644
index 000000000..23e490f6e
--- /dev/null
+++ b/projects/common/components/trigger/trigger.component.ts
@@ -0,0 +1,41 @@
+import { ElementComponent } from 'common/directives/element-component.directive';
+import {
+  Component, EventEmitter, Input, Output
+} from '@angular/core';
+import { TriggerElement, TriggerActionEvent } from 'common/models/elements/trigger/trigger';
+
+@Component({
+  selector: 'aspect-trigger',
+  template: `
+    <div>
+      <div *ngIf="project === 'editor'"
+            class="hidden-trigger">
+      </div>
+    </div>
+  `,
+  styles: [`
+    .hidden-trigger {
+      height: 20px;
+      background-image: linear-gradient(
+        135deg, #fff 45%, #999 45%, #999 50%, #fff 50%, #fff 95%, #999 95%, #999 100%
+      );
+      background-size: 10px 10px;
+      border: 1px solid #999;
+    }
+  `]
+})
+
+export class TriggerComponent extends ElementComponent {
+  @Input() elementModel!: TriggerElement;
+  @Output() triggerActionEvent = new EventEmitter<TriggerActionEvent>();
+
+  emitEvent(): void {
+    if (this.elementModel.action && this.elementModel.actionParam) {
+      this.triggerActionEvent
+        .emit({
+          action: this.elementModel.action,
+          param: this.elementModel.actionParam
+        });
+    }
+  }
+}
diff --git a/projects/common/models/elements/element.ts b/projects/common/models/elements/element.ts
index 8ce263e02..b7bd7d6cc 100644
--- a/projects/common/models/elements/element.ts
+++ b/projects/common/models/elements/element.ts
@@ -25,7 +25,7 @@ import { VariableInfo } from '@iqb/responses';
 export type UIElementType = 'text' | 'button' | 'text-field' | 'text-field-simple' | 'text-area' | 'checkbox'
 | 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert-row' | 'radio-group-images' | 'hotspot-image'
 | 'drop-list' | 'cloze' | 'spell-correct' | 'slider' | 'frame' | 'toggle-button' | 'geometry'
-| 'math-field' | 'math-table' | 'text-area-math';
+| 'math-field' | 'math-table' | 'text-area-math' | 'trigger';
 
 export interface OptionElement extends UIElement {
   getNewOptionLabel(optionText: string): Label;
diff --git a/projects/common/models/elements/trigger/trigger.ts b/projects/common/models/elements/trigger/trigger.ts
new file mode 100644
index 000000000..e79f0800b
--- /dev/null
+++ b/projects/common/models/elements/trigger/trigger.ts
@@ -0,0 +1,55 @@
+import { Type } from '@angular/core';
+import {
+  UIElement, UIElementProperties, UIElementType
+} from 'common/models/elements/element';
+import { ElementComponent } from 'common/directives/element-component.directive';
+import { StateVariable } from 'common/models/state-variable';
+import { environment } from 'common/environment';
+import { InstantiationEror } from 'common/util/errors';
+import { TriggerComponent } from 'common/components/trigger/trigger.component';
+
+export class TriggerElement extends UIElement implements TriggerProperties {
+  type: UIElementType = 'trigger';
+  action: null | TriggerAction = null;
+  actionParam: null | string | StateVariable = null;
+
+  constructor(element?: TriggerProperties) {
+    super(element);
+    if (element && isValid(element)) {
+      this.action = element.action;
+      this.actionParam = element.actionParam;
+    } else {
+      if (environment.strictInstantiation) {
+        throw new InstantiationEror('Error at Trigger instantiation', element);
+      }
+      if (element?.action !== undefined) this.action = element.action;
+      if (element?.actionParam !== undefined) this.actionParam = element.actionParam;
+    }
+  }
+
+  getDuplicate(): TriggerElement {
+    return new TriggerElement(this);
+  }
+
+  getElementComponent(): Type<ElementComponent> {
+    return TriggerComponent;
+  }
+}
+
+export interface TriggerProperties extends UIElementProperties {
+  action: null | TriggerAction;
+  actionParam: null | string | StateVariable ;
+}
+
+function isValid(blueprint?: TriggerProperties): boolean {
+  if (!blueprint) return false;
+  return blueprint.action !== undefined &&
+    blueprint.actionParam !== undefined;
+}
+
+export interface TriggerActionEvent {
+  action: TriggerAction;
+  param: string | StateVariable;
+}
+
+export type TriggerAction = 'highlightText' | 'stateVariableChange';
diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts
index 14a5cc7bb..531ff26c4 100644
--- a/projects/common/shared.module.ts
+++ b/projects/common/shared.module.ts
@@ -30,6 +30,7 @@ import { TooltipEventTooltipDirective } from 'common/components/tooltip/tooltip-
 import { TooltipComponent } from 'common/components/tooltip/tooltip.component';
 import { PointerEventTooltipDirective } from 'common/components/tooltip/pointer-event-tooltip.directive';
 import { ClozeChildErrorMessage } from 'common/components/compound-elements/cloze/cloze-child-error-message';
+import { TriggerComponent } from 'common/components/trigger/trigger.component';
 import { TextComponent } from './components/text/text.component';
 import { ButtonComponent } from './components/button/button.component';
 import { TextFieldComponent } from './components/input-elements/text-field.component';
@@ -116,6 +117,7 @@ import { TextAreaMathComponent } from './components/input-elements/text-area-mat
   ],
   declarations: [
     ButtonComponent,
+    TriggerComponent,
     TextComponent,
     TextFieldComponent,
     TextFieldSimpleComponent,
@@ -207,6 +209,7 @@ import { TextAreaMathComponent } from './components/input-elements/text-area-mat
     HotspotImageComponent,
     LikertComponent,
     ButtonComponent,
+    TriggerComponent,
     FrameComponent,
     ImageComponent,
     GeometryComponent,
diff --git a/projects/common/util/element.factory.ts b/projects/common/util/element.factory.ts
index acc7f5885..b0c4f9aef 100644
--- a/projects/common/util/element.factory.ts
+++ b/projects/common/util/element.factory.ts
@@ -26,11 +26,13 @@ import { HotspotImageElement } from 'common/models/elements/input-elements/hotsp
 import { MathFieldElement } from 'common/models/elements/input-elements/math-field';
 import { MathTableElement } from 'common/models/elements/input-elements/math-table';
 import { TextAreaMathElement } from 'common/models/elements/input-elements/text-area-math';
+import { TriggerElement } from 'common/models/elements/trigger/trigger';
 
 export abstract class ElementFactory {
   static ELEMENT_CLASSES: Record<string, Type<UIElement>> = {
     text: TextElement,
     button: ButtonElement,
+    trigger: TriggerElement,
     'text-field': TextFieldElement,
     'text-field-simple': TextFieldSimpleElement,
     'text-area': TextAreaElement,
diff --git a/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.html b/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.html
index 4a326ba64..f117b4da0 100644
--- a/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.html
+++ b/projects/editor/src/app/components/new-ui-element-panel/ui-element-toolbox.component.html
@@ -153,6 +153,11 @@
           <mat-icon>architecture</mat-icon>
           {{'toolbox.geometry' | translate }}
         </button>
+        <button mat-stroked-button (click)="addUIElement('trigger')"
+                draggable="true" (dragstart)="$event.dataTransfer?.setData('elementType','trigger')">
+          <mat-icon>bolt</mat-icon>
+          {{'toolbox.trigger' | translate }}
+        </button>
       </mat-expansion-panel>
     </mat-accordion>
   </div>
diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html
index 601c91b4f..367cdcf4c 100644
--- a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html
+++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.html
@@ -134,6 +134,14 @@
                             (updateModel)="updateModel.emit($event)">
   </aspect-button-properties>
 
+  <aspect-action-properties *ngIf="combinedProperties.action !== undefined"
+    [actions]="combinedProperties.type === 'button' ? ['unitNav', 'pageNav', 'highlightText', 'stateVariableChange'] :
+                                                      ['highlightText', 'stateVariableChange']"
+    [combinedProperties]="combinedProperties"
+    (updateModel)="updateModel.emit($event)">
+  </aspect-action-properties>
+
+
   <aspect-slider-properties [combinedProperties]="combinedProperties"
                             (updateModel)="updateModel.emit($event)">
   </aspect-slider-properties>
diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/action-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/action-properties.component.ts
index 0402f5e5a..d7c3dc9b1 100644
--- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/action-properties.component.ts
+++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/action-properties.component.ts
@@ -22,7 +22,7 @@ import { Page } from 'common/models/page';
               <mat-option [value]="null">
                 {{ 'propertiesPanel.none' | translate }}
               </mat-option>
-              <mat-option *ngFor="let option of ['unitNav', 'pageNav', 'highlightText', 'stateVariableChange']"
+              <mat-option *ngFor="let option of actions"
                           [value]="option">
                 {{ 'propertiesPanel.' + option | translate }}
               </mat-option>
@@ -88,6 +88,7 @@ import { Page } from 'common/models/page';
 
 export class ActionPropertiesComponent {
   @Input() combinedProperties!: UIElement;
+  @Input() actions!: string[];
   @Output() updateModel =
     new EventEmitter<{
       property: string; value: string | number | boolean | StateVariable | null, isInputValid?: boolean | null
diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties.component.ts
index 0e33cfc27..3720bff26 100644
--- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties.component.ts
+++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties.component.ts
@@ -3,7 +3,6 @@ import {
 } from '@angular/core';
 import { FileService } from 'common/services/file.service';
 import { UIElement } from 'common/models/elements/element';
-import { StateVariable } from 'common/models/state-variable';
 
 @Component({
   selector: 'aspect-button-properties',
@@ -84,10 +83,6 @@ import { StateVariable } from 'common/models/state-variable';
           </mat-form-field>
         </div>
       </fieldset>
-      <aspect-action-properties
-        [combinedProperties]="combinedProperties"
-        (updateModel)="updateModel.emit($event)">
-      </aspect-action-properties>
     </ng-container>
   `,
   styles: [`
@@ -125,7 +120,7 @@ export class ButtonPropertiesComponent {
   @Input() combinedProperties!: UIElement;
   @Output() updateModel =
     new EventEmitter<{
-      property: string; value: string | number | boolean | StateVariable | null, isInputValid?: boolean | null
+      property: string; value: string | number | boolean | null, isInputValid?: boolean | null
     }>();
 
   checked = false;
diff --git a/projects/editor/src/app/services/id.service.ts b/projects/editor/src/app/services/id.service.ts
index bbb03c3b5..a5c5edd51 100644
--- a/projects/editor/src/app/services/id.service.ts
+++ b/projects/editor/src/app/services/id.service.ts
@@ -39,7 +39,8 @@ export class IDService {
     'math-table': 0,
     value: 0,
     'state-variable': 0,
-    'text-area-math': 0
+    'text-area-math': 0,
+    trigger: 0
   };
 
   constructor(private messageService: MessageService, private translateService: TranslateService) { }
diff --git a/projects/editor/src/assets/i18n/de.json b/projects/editor/src/assets/i18n/de.json
index 9caee642c..d20552966 100644
--- a/projects/editor/src/assets/i18n/de.json
+++ b/projects/editor/src/assets/i18n/de.json
@@ -327,7 +327,8 @@
     "formula": "Formel",
     "math-field": "Feld",
     "math-area": "Bereich",
-    "math-table": "Rechenkästchen"
+    "math-table": "Rechenkästchen",
+    "trigger": "Auslöser"
   },
   "section-menu": {
     "height": "Höhe",
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 90fcaf5bb..45f615daf 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
@@ -23,7 +23,7 @@ export class ElementGroupSelectionComponent implements OnInit {
     },
     { name: 'compoundGroup', types: ['cloze', 'likert'] },
     { name: 'textGroup', types: ['text'] },
-    { name: 'interactiveGroup', types: ['button', 'image', 'math-table'] },
+    { name: 'interactiveGroup', types: ['button', 'image', 'math-table', 'trigger'] },
     { name: 'externalAppGroup', types: ['geometry'] }
   ];
 
diff --git a/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.html b/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.html
index a76e6c846..e082902ee 100644
--- a/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.html
+++ b/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.html
@@ -19,6 +19,15 @@
   (focusChanged)="toggleKeyInput($event)"
   (elementValueChanged)="changeElementCodeValue($event)">
 </aspect-math-table>
+<aspect-trigger
+  *ngIf="elementModel.type === 'trigger'"
+  #elementComponent
+  [elementModel]="elementModel | cast: TriggerElement"
+  (triggerActionEvent)="applyTriggerAction($event)"
+  aspectInViewDetection
+  detectionType="top"
+  (intersecting)="elementComponent.emitEvent()">>
+</aspect-trigger>
 
 <aspect-floating-keypad
   [isKeypadOpen]="isKeypadOpen && keypadService.position === 'floating'">
diff --git a/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.ts b/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.ts
index 2342c9d8e..1f9fc5396 100644
--- a/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.ts
+++ b/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.ts
@@ -3,6 +3,7 @@ import {
 } from '@angular/core';
 import { ElementComponent } from 'common/directives/element-component.directive';
 import { ButtonElement, ButtonEvent, UnitNavParam } from 'common/models/elements/button/button';
+import { TriggerActionEvent, TriggerElement } from 'common/models/elements/trigger/trigger';
 import { ImageElement } from 'common/models/elements/media-elements/image';
 import { UIElement, ValueChangeElement } from 'common/models/elements/element';
 import { VeronaPostService } from 'player/modules/verona/services/verona-post.service';
@@ -29,6 +30,7 @@ export class InteractiveGroupElementComponent extends ElementGroupDirective impl
   ButtonElement!: ButtonElement;
   ImageElement!: ImageElement;
   MathTableElement!: MathTableElement;
+  TriggerElement!: TriggerElement;
 
   tableModel: MathTableRow[] = [];
 
@@ -92,12 +94,19 @@ export class InteractiveGroupElementComponent extends ElementGroupDirective impl
       case 'pageNav':
         this.navigationService.setPage(buttonEvent.param as number);
         break;
+      default:
+        this.applyTriggerAction(buttonEvent as TriggerActionEvent);
+    }
+  }
+
+  applyTriggerAction(triggerActionEvent: TriggerActionEvent): void {
+    switch (triggerActionEvent.action) {
       case 'highlightText':
-        this.anchorService.toggleAnchor(buttonEvent.param as string);
+        this.anchorService.toggleAnchor(triggerActionEvent.param as string);
         break;
       case 'stateVariableChange':
         this.stateVariableStateService.changeElementCodeValue(
-          buttonEvent.param as { id: string, value: string }
+          triggerActionEvent.param as { id: string, value: string }
         );
         break;
       default:
diff --git a/projects/player/src/app/directives/in-view-detection.directive.ts b/projects/player/src/app/directives/in-view-detection.directive.ts
index 8b3d81113..eb35957e3 100644
--- a/projects/player/src/app/directives/in-view-detection.directive.ts
+++ b/projects/player/src/app/directives/in-view-detection.directive.ts
@@ -21,7 +21,7 @@ export class InViewDetectionDirective implements AfterViewInit, OnDestroy {
   constructor(private elementRef: ElementRef) {}
 
   ngAfterViewInit(): void {
-    const intersectionContainer = this.elementRef.nativeElement.closest('aspect-page-scroll-button');
+    const intersectionContainer = this.elementRef.nativeElement.closest('aspect-page-scroll-button') || document;
     if (intersectionContainer) {
       const constraint = this.detectionType === 'top' ? '0px' : '0px';
       this.intersectionDetector = new IntersectionDetector(intersectionContainer, constraint);
-- 
GitLab