diff --git a/projects/common/assets/common-styles.css b/projects/common/assets/common-styles.css index eca1f2c3254c2da4f711cfa9eeff211c52dcec38..d0431525cb19d47a514e22d5287c223315b5e4c5 100644 --- a/projects/common/assets/common-styles.css +++ b/projects/common/assets/common-styles.css @@ -66,3 +66,32 @@ blockquote p { .cdk-drag-dragging { cursor: grabbing; } + +.fx-row-start-stretch { + display: flex; + align-items: stretch; + flex-direction: row; + justify-content: flex-start; +} + +.fx-row-end-stretch { + display: flex; + align-items: stretch; + flex-direction: row; + justify-content: flex-end; +} + +.fx-colum-start-stretch { + display: flex; + align-items: stretch; + flex-direction: column; + justify-content: flex-start; +} + +.fx-gap-10 { + gap: 10px; +} + +.fx-gap-20 { + gap: 20px; +} diff --git a/projects/common/models/elements/button/button.ts b/projects/common/models/elements/button/button.ts index 52882f6e2dfd7204eb20c2d35fbda254866724cb..4b6a407e2b6f9d8098964fd4e7b66c040871f1bb 100644 --- a/projects/common/models/elements/button/button.ts +++ b/projects/common/models/elements/button/button.ts @@ -3,13 +3,14 @@ import { UIElement } from 'common/models/elements/element'; import { ButtonComponent } from 'common/components/button/button.component'; import { ElementComponent } from 'common/directives/element-component.directive'; import { BasicStyles, PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { StateVariable } from 'common/models/state-variable'; export interface ButtonEvent { action: ButtonAction; - param: UnitNavParam | number | string; + param: UnitNavParam | number | string | StateVariable } -export type ButtonAction = 'unitNav' | 'pageNav' | 'highlightText'; +export type ButtonAction = 'unitNav' | 'pageNav' | 'highlightText' | 'stateVariableChange'; export type UnitNavParam = 'previous' | 'next' | 'first' | 'last' | 'end'; export class ButtonElement extends UIElement { @@ -17,7 +18,7 @@ export class ButtonElement extends UIElement { imageSrc: string | null = null; asLink: boolean = false; action: null | ButtonAction = null; - actionParam: null | UnitNavParam | number | string = null; + actionParam: null | UnitNavParam | number | string | StateVariable = null; position: PositionProperties | undefined; styling: BasicStyles & { borderRadius: number; diff --git a/projects/common/models/elements/element.ts b/projects/common/models/elements/element.ts index 16232ae890ba3ae0696839b11856c6fa5d785806..f7dcd1d1da88dc0f6539c886188a3023a9a9b16a 100644 --- a/projects/common/models/elements/element.ts +++ b/projects/common/models/elements/element.ts @@ -10,6 +10,8 @@ import { BasicStyles, ExtendedStyles, DimensionProperties, PlayerProperties, PositionProperties } from 'common/models/elements/property-group-interfaces'; +import { VisibilityRule } from 'common/models/visibility-rule'; +import { StateVariable } from 'common/models/state-variable'; 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' @@ -31,8 +33,8 @@ export interface ValueChangeElement { } export type UIElementValue = string | number | boolean | undefined | UIElementType | InputElementValue | -TextLabel | TextLabel[] | ClozeDocument | LikertRowElement[] | Hotspot[] | -PositionProperties | PlayerProperties | BasicStyles | Measurement | Measurement[]; +TextLabel | TextLabel[] | ClozeDocument | LikertRowElement[] | Hotspot[] | StateVariable | +PositionProperties | PlayerProperties | BasicStyles | Measurement | Measurement[] | VisibilityRule[]; export type InputAssistancePreset = null | 'french' | 'numbers' | 'numbersAndOperators' | 'numbersAndBasicOperators' | 'comparisonOperators' | 'squareDashDot' | 'placeValue' | 'space' | 'comma' | 'custom'; diff --git a/projects/common/models/section.ts b/projects/common/models/section.ts index 753c6ab6f684380745bee7ddf2986bc00c163353..a42e8ba8fdfefce61c25283b1e07a79fe63aebee 100644 --- a/projects/common/models/section.ts +++ b/projects/common/models/section.ts @@ -10,6 +10,7 @@ import { TextElement } from 'common/models/elements/text/text'; import { ImageElement } from 'common/models/elements/media-elements/image'; import { ElementFactory } from 'common/util/element.factory'; import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; +import { VisibilityRule } from 'common/models/visibility-rule'; export class Section { [index: string]: unknown; @@ -23,6 +24,7 @@ export class Section { gridRowSizes: { value: number; unit: string }[] = [{ value: 1, unit: 'fr' }]; activeAfterID: string | null = null; activeAfterIdDelay: number = 0; + visibilityRules: VisibilityRule[] = []; constructor(blueprint?: Record<string, any>) { const sanitizedBlueprint = Section.sanitizeBlueprint(blueprint); @@ -35,6 +37,10 @@ export class Section { if (sanitizedBlueprint.gridRowSizes !== undefined) this.gridRowSizes = sanitizedBlueprint.gridRowSizes; if (sanitizedBlueprint.activeAfterID) this.activeAfterID = sanitizedBlueprint.activeAfterID; if (sanitizedBlueprint.activeAfterIdDelay) this.activeAfterIdDelay = sanitizedBlueprint.activeAfterIdDelay; + if (sanitizedBlueprint.visibilityRules) { + this.visibilityRules = sanitizedBlueprint.visibilityRules + .map(rule => new VisibilityRule(rule.id, rule.operator, rule.value)); + } this.elements = sanitizedBlueprint.elements?.map(element => ElementFactory.createElement({ ...element, diff --git a/projects/common/models/state-variable.ts b/projects/common/models/state-variable.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9d37afd009ddb0407838ab7144d2c4b5932b71c --- /dev/null +++ b/projects/common/models/state-variable.ts @@ -0,0 +1,8 @@ +export class StateVariable { + id: string; + value: string; + constructor(id: string, value: string) { + this.id = id; + this.value = value; + } +} diff --git a/projects/common/models/unit.ts b/projects/common/models/unit.ts index 107157cae57ed1c24e0579ff6ecd55d1e2635c85..57f3628cdd9410690fa7b89bceced926ed1f49c4 100644 --- a/projects/common/models/unit.ts +++ b/projects/common/models/unit.ts @@ -1,16 +1,18 @@ -import packageJSON from '../../../package.json'; import { Page } from 'common/models/page'; import { UIElement } from 'common/models/elements/element'; - import { AnswerScheme } from 'common/models/elements/answer-scheme-interfaces'; +import { StateVariable } from 'common/models/state-variable'; +import packageJSON from '../../../package.json'; export class Unit { type = 'aspect-unit-definition'; version: string; + stateVariables: StateVariable[] = []; pages: Page[] = []; constructor(unit?: Partial<Unit>) { this.version = packageJSON.config.unit_definition_version; + this.stateVariables = unit?.stateVariables || []; this.pages = unit?.pages?.map(page => new Page(page)) || [new Page()]; } diff --git a/projects/common/models/visibility-rule.ts b/projects/common/models/visibility-rule.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a1856d9e8fe5d62638c9382aa53d2ddb9716f36 --- /dev/null +++ b/projects/common/models/visibility-rule.ts @@ -0,0 +1,15 @@ +export class VisibilityRule { + id: string; + operator: Operator; + value: string; + + static operators = ['=', '!=', '<', '<=', '>', '>=']; + + constructor(id: string, operator: Operator, value: string) { + this.id = id; + this.operator = operator; + this.value = value; + } +} + +export type Operator = typeof VisibilityRule.operators[number]; diff --git a/projects/editor/src/app/app.module.ts b/projects/editor/src/app/app.module.ts index 2f64f14c8011eb4fdf84f547248fbd4f15cb9e71..d8b7855bfe442a9eae17fbc393dfa17ada7ed8e3 100644 --- a/projects/editor/src/app/app.module.ts +++ b/projects/editor/src/app/app.module.ts @@ -27,6 +27,25 @@ import { } from 'editor/src/app/components/properties-panel/model-properties-tab/input-groups/hotspot-field-set.component'; import { HotspotEditDialogComponent } from 'editor/src/app/components/dialogs/hotspot-edit-dialog.component'; import { MathEditorModule } from 'common/math-editor.module'; +import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay'; +import { + StateVariablesDialogComponent +} from 'editor/src/app/components/dialogs/state-variables-dialog/state-variables-dialog.component'; +import { + VisibilityRuleEditorComponent +} from 'editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rule-editor.component'; +import { + ShowStateVariablesButtonComponent +} from 'editor/src/app/components/new-ui-element-panel/state-variables-button/show-state-variables-button.component'; +import { + StateVariableEditorComponent +} from 'editor/src/app/components/dialogs/state-variables-dialog/state-variable-editor.component'; +import { + ButtonActionParamStateVariableComponent +} from 'editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/button-action-param-state-variable.component'; +import { + VisibilityRulesDialogComponent +} from 'editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component'; import { AppComponent } from './app.component'; import { ToolbarComponent } from './components/toolbar/toolbar.component'; import { UiElementToolboxComponent } from @@ -73,7 +92,7 @@ import { OptionsFieldSetComponent } from import { TextPropertiesFieldSetComponent } from './components/properties-panel/model-properties-tab/input-groups/text-properties-field-set.component'; import { ButtonPropertiesComponent, GetAnchorIdsPipe, ScrollPageIndexPipe } from - './components/properties-panel/model-properties-tab/input-groups/button-properties.component'; + './components/properties-panel/model-properties-tab/input-groups/button-properties/button-properties.component'; import { SliderPropertiesComponent } from './components/properties-panel/model-properties-tab/input-groups/slider-properties.component'; import { TextFieldElementPropertiesComponent } from @@ -99,7 +118,9 @@ import { import { GeogebraAppDefinitionDialogComponent } from './components/dialogs/geogebra-app-definition-dialog.component'; import { SizeInputPanelComponent } from './components/util/size-input-panel.component'; import { ComboButtonComponent } from './components/util/combo-button.component'; -import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay'; +import { + GetStateVariableIdsPipe +} from './components/properties-panel/model-properties-tab/input-groups/button-properties/get-state-variable-ids.pipe'; @NgModule({ declarations: [ @@ -158,7 +179,14 @@ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay'; GetAnchorIdsPipe, ScrollPageIndexPipe, SizeInputPanelComponent, - ComboButtonComponent + ComboButtonComponent, + VisibilityRuleEditorComponent, + StateVariablesDialogComponent, + ShowStateVariablesButtonComponent, + StateVariableEditorComponent, + ButtonActionParamStateVariableComponent, + GetStateVariableIdsPipe, + VisibilityRulesDialogComponent ], imports: [ BrowserModule, diff --git a/projects/editor/src/app/components/canvas/section-menu.component.ts b/projects/editor/src/app/components/canvas/section-menu.component.ts index 0621a80aeee5d2171c0a99de0055c3d25808b296..adf2dbdb78ab13cbe0a108af0a8b54df3814d92e 100644 --- a/projects/editor/src/app/components/canvas/section-menu.component.ts +++ b/projects/editor/src/app/components/canvas/section-menu.component.ts @@ -1,6 +1,5 @@ import { - Component, OnDestroy, Input, Output, EventEmitter, - ViewChild, ElementRef + Component, OnDestroy, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -10,6 +9,7 @@ import { UIElement } from 'common/models/elements/element'; import { Section } from 'common/models/section'; import { DropListElement } from 'common/models/elements/input-elements/drop-list'; import { IDService } from 'editor/src/app/services/id.service'; +import { VisibilityRule } from 'common/models/visibility-rule'; import { UnitService } from '../../services/unit.service'; import { DialogService } from '../../services/dialog.service'; import { SelectionService } from '../../services/selection.service'; @@ -42,28 +42,59 @@ import { SelectionService } from '../../services/selection.service'; [value]="$any(section.backgroundColor)" (change)="updateModel('backgroundColor', $any($event.target).value)"> - <button mat-mini-fab [matMenuTriggerFor]="activeAfterIDMenu" + <button mat-mini-fab + (click)="showVisibilityRulesDialog()" [matTooltip]="'Sichtbarkeit'" [matTooltipPosition]="'left'"> <mat-icon>disabled_visible</mat-icon> </button> - <mat-menu #activeAfterIDMenu="matMenu" - class="activeAfterID-menu" xPosition="before"> - <mat-form-field appearance="outline"> - <mat-label>{{'section-menu.activeAfterID' | translate }}</mat-label> - <input matInput - [value]="$any(section.activeAfterID)" - (click)="$any($event).stopPropagation()" - (change)="updateModel('activeAfterID', $any($event.target).value)"> - </mat-form-field> - <mat-form-field appearance="outline"> - <mat-label>{{'section-menu.activeAfterIdDelay' | translate }}</mat-label> - <input matInput type="number" step="1000" min="0" - [disabled]="!section.activeAfterID" - [value]="$any(section.activeAfterIdDelay)" - (click)="$any($event).stopPropagation()" - (change)="updateModel('activeAfterIdDelay', $any($event.target).value)"> - </mat-form-field> - </mat-menu> + <!-- <mat-menu #activeAfterIDMenu="matMenu"--> + <!-- class="activeAfterID-menu" xPosition="before">--> + + <!-- <aspect-rules [elementIds]="elementIds"--> + <!-- [rules]="section.rules"--> + <!-- (click)="$event.stopPropagation()">--> + <!-- </aspect-rules>--> + + + <!-- <mat-form-field appearance="outline">--> + <!-- <mat-label>{{'section-menu.activeAfterID' | translate }}</mat-label>--> + <!-- <input matInput--> + <!-- [value]="$any(section.activeAfterID)"--> + <!-- (click)="$any($event).stopPropagation()"--> + <!-- (change)="updateModel('activeAfterID', $any($event.target).value)">--> + <!-- </mat-form-field>--> + <!-- <mat-form-field appearance="outline">--> + <!-- <mat-label>{{'section-menu.activeAfterIdDelay' | translate }}</mat-label>--> + <!-- <input matInput type="number" step="1000" min="0"--> + <!-- [disabled]="!section.activeAfterID"--> + <!-- [value]="$any(section.activeAfterIdDelay)"--> + <!-- (click)="$any($event).stopPropagation()"--> + <!-- (change)="updateModel('activeAfterIdDelay', $any($event.target).value)">--> + <!-- </mat-form-field>--> + + + <!-- <button mat-icon-button--> + <!-- matSuffix--> + <!-- color="primary"--> + <!-- (click)="addRule($event)">--> + <!-- <mat-icon>add</mat-icon>--> + <!-- </button>--> + <!-- <ng-container *ngFor="let rule of section.rules; let i = index">--> + <!-- <aspect-rule [elementIds]="elementIds"--> + <!-- [rule]="rule"--> + <!-- (ruleChange)="updateRule(i, $event)"--> + <!-- (click)="$event.stopPropagation()">--> + <!-- </aspect-rule>--> + <!-- <button mat-icon-button--> + <!-- matSuffix--> + <!-- color="primary"--> + <!-- (click)="deleteRule(i)">--> + <!-- <mat-icon>delete</mat-icon>--> + <!-- </button>--> + <!-- </ng-container>--> + + + <!-- </mat-menu>--> <button mat-mini-fab [matMenuTriggerFor]="layoutMenu" [matTooltip]="'Layout'" [matTooltipPosition]="'left'"> <mat-icon>space_dashboard</mat-icon> @@ -206,7 +237,9 @@ export class SectionMenuComponent implements OnDestroy { private idService: IDService, private clipboard: Clipboard) { } - updateModel(property: string, value: string | number | boolean | { value: number; unit: string }[]): void { + updateModel( + property: string, value: string | number | boolean | VisibilityRule[] | { value: number; unit: string }[] + ): void { this.unitService.updateSectionProperty(this.section, property, value); } @@ -285,4 +318,21 @@ export class SectionMenuComponent implements OnDestroy { } }); } + + showVisibilityRulesDialog(): void { + this.dialogService.showVisibilityRulesDialog(this.section.visibilityRules, this.getControlIds(), this.section.activeAfterIdDelay) + .subscribe(rulesWithDelay => { + if (rulesWithDelay) { + this.updateModel('visibilityRules', rulesWithDelay.visibilityRules); + this.updateModel('activeAfterIdDelay', rulesWithDelay.activeAfterIdDelay); + } + }); + } + + private getControlIds(): string[] { + return this.unitService.unit.getAllElements() + .map(element => element.id) + .concat(this.unitService.unit.stateVariables + .map(element => element.id)); + } } diff --git a/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variable-editor.component.html b/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variable-editor.component.html new file mode 100644 index 0000000000000000000000000000000000000000..80dbcec4c52796e5eaa7b7011c4ab622aede91db --- /dev/null +++ b/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variable-editor.component.html @@ -0,0 +1,19 @@ +<div class="fx-row-start-stretch fx-gap-10"> + <mat-form-field> + <mat-label>Id</mat-label> + <input matInput + placeholder="Id" + [(ngModel)]="stateVariable.id" + (ngModelChange)="stateVariableChange.emit(stateVariable)"> + </mat-form-field> + + <mat-form-field> + <mat-label>Wert</mat-label> + <input matInput + placeholder="Initialwert" + [(ngModel)]="stateVariable.value" + (ngModelChange)="stateVariableChange.emit(stateVariable)"> + </mat-form-field> +</div> + + diff --git a/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variable-editor.component.ts b/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variable-editor.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee07eb08fb0a4fdf41f72fa75f5c635549538ddd --- /dev/null +++ b/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variable-editor.component.ts @@ -0,0 +1,13 @@ +import { + Component, EventEmitter, Input, Output +} from '@angular/core'; +import { StateVariable } from 'common/models/state-variable'; + +@Component({ + selector: 'aspect-state-variable-editor', + templateUrl: './state-variable-editor.component.html' +}) +export class StateVariableEditorComponent { + @Input() stateVariable: StateVariable = new StateVariable('', ''); + @Output() stateVariableChange = new EventEmitter<StateVariable>(); +} diff --git a/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variables-dialog.component.html b/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variables-dialog.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4195dc5dfca57714b8d21187842b0a16f91b34c5 --- /dev/null +++ b/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variables-dialog.component.html @@ -0,0 +1,33 @@ +<h1 mat-dialog-title>Player-Variablen bearbeiten</h1> +<div mat-dialog-content class="fx-colum-start-stretch fx-gap-20"> + <div class="fx-row-end-stretch"> + <button mat-icon-button + matSuffix + color="primary" + (click)="addStateVariable()"> + <mat-icon>add</mat-icon> + </button> + </div> + <div *ngFor="let stateVariable of stateVariables; let i = index" + class="fx-row-start-stretch fx-gap-10"> + <aspect-state-variable-editor + [(stateVariable)]="stateVariables[i]"> + </aspect-state-variable-editor> + <button mat-icon-button + matSuffix + color="warn" + (click)="deleteStateVariable(i)"> + <mat-icon>delete</mat-icon> + </button> + </div> +</div> +<div mat-dialog-actions> + <button mat-button + mat-dialog-close> + Abbrechen + </button> + <button mat-button + [mat-dialog-close]="stateVariables"> + Speichern + </button> +</div> diff --git a/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variables-dialog.component.ts b/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variables-dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c76afdd04be628ff59b294b90bceebb9f68b94d0 --- /dev/null +++ b/projects/editor/src/app/components/dialogs/state-variables-dialog/state-variables-dialog.component.ts @@ -0,0 +1,22 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { StateVariable } from 'common/models/state-variable'; + +@Component({ + templateUrl: './state-variables-dialog.component.html' +}) +export class StateVariablesDialogComponent { + stateVariables: StateVariable[]; + + constructor(@Inject(MAT_DIALOG_DATA) private data: { stateVariables: StateVariable[] }) { + this.stateVariables = [...data.stateVariables]; + } + + addStateVariable() { + this.stateVariables.push(new StateVariable('NewState', '1')); + } + + deleteStateVariable(index: number) { + this.stateVariables.splice(index, 1); + } +} diff --git a/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rule-editor.component.html b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rule-editor.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4b0481689cc4548309d1e5f16fbf798621b230fb --- /dev/null +++ b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rule-editor.component.html @@ -0,0 +1,31 @@ +<div class="fx-row-start-stretch fx-gap-10"> + <mat-form-field> + <mat-label>Id</mat-label> + <mat-select [(ngModel)]="visibilityRule.id" + (ngModelChange)="visibilityRuleChange.emit(visibilityRule)"> + <mat-option *ngFor="let id of controlIds" + [value]="id"> + {{id}} + </mat-option> + </mat-select> + </mat-form-field> + + <mat-form-field> + <mat-label>Operator</mat-label> + <mat-select [(ngModel)]="visibilityRule.operator" + (ngModelChange)="visibilityRuleChange.emit(visibilityRule)"> + <mat-option *ngFor="let operator of VisibilityRule.operators" + [value]="operator"> + {{operator}} + </mat-option> + </mat-select> + </mat-form-field> + + <mat-form-field> + <mat-label>Wert</mat-label> + <input matInput + placeholder="Wert" + [(ngModel)]="visibilityRule.value" + (ngModelChange)="visibilityRuleChange.emit(visibilityRule)"> + </mat-form-field> +</div> diff --git a/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rule-editor.component.ts b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rule-editor.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..24547874b80fcfc6d8b2414567f889f4c64ad5aa --- /dev/null +++ b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rule-editor.component.ts @@ -0,0 +1,16 @@ +import { + Component, EventEmitter, Input, Output +} from '@angular/core'; +import { VisibilityRule } from 'common/models/visibility-rule'; + +@Component({ + selector: 'aspect-visibility-rule-editor', + templateUrl: './visibility-rule-editor.component.html' +}) +export class VisibilityRuleEditorComponent { + @Input() controlIds!: string[]; + @Input() visibilityRule!: VisibilityRule; + + @Output() visibilityRuleChange = new EventEmitter<VisibilityRule>(); + protected readonly VisibilityRule = VisibilityRule; +} diff --git a/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.html b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.html new file mode 100644 index 0000000000000000000000000000000000000000..901835660fc1e53b511dcc3725df60cc70a7ed4c --- /dev/null +++ b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.html @@ -0,0 +1,45 @@ +<h1 mat-dialog-title>Regeln zur Sichtbarkeit</h1> +<div mat-dialog-content class="fx-colum-start-stretch fx-gap-20"> + <ng-container *ngIf="controlIds.length"> + <div class="fx-row-end-stretch"> + <button mat-icon-button + matSuffix + color="primary" + (click)="addVisibilityRule()"> + <mat-icon>add</mat-icon> + </button> + </div> + <mat-form-field *ngIf="visibilityRules.length"> + <mat-label>Verzögerung in ms</mat-label> + <input matInput + placeholder="Verzögerung in ms" + [(ngModel)]="activeAfterIdDelay"> + </mat-form-field> + </ng-container> + + <p *ngIf="!controlIds.length">Bitte zuerst Elemente oder Player-Variablen anlegen</p> + + <div *ngFor="let rule of visibilityRules; let i = index" + class="fx-row-start-stretch fx-gap-10"> + <aspect-visibility-rule-editor [controlIds]="controlIds" + [(visibilityRule)]="visibilityRules[i]"> + </aspect-visibility-rule-editor> + <button mat-icon-button + matSuffix + color="warn" + (click)="deleteVisibilityRule(i)"> + <mat-icon>delete</mat-icon> + </button> + </div> +</div> +<div mat-dialog-actions> + <button mat-button + mat-dialog-close> + Abbrechen + </button> + <button *ngIf="controlIds.length" + mat-button + [mat-dialog-close]="{visibilityRules:visibilityRules, activeAfterIdDelay}"> + Speichern + </button> +</div> diff --git a/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.ts b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a8f7babdbc190ce6f54336fc9ea7a596cffb8f90 --- /dev/null +++ b/projects/editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component.ts @@ -0,0 +1,32 @@ +import { Component, Inject } from '@angular/core'; +import { VisibilityRule } from 'common/models/visibility-rule'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; + +@Component({ + templateUrl: './visibility-rules-dialog.component.html' +}) +export class VisibilityRulesDialogComponent { + visibilityRules!: VisibilityRule[]; + controlIds!: string[]; + activeAfterIdDelay!: number; + + constructor( + @Inject(MAT_DIALOG_DATA) private data: { + visibilityRules: VisibilityRule[], + activeAfterIdDelay: number, + controlIds: string[], + } + ) { + this.visibilityRules = [...data.visibilityRules]; + this.activeAfterIdDelay = data.activeAfterIdDelay; + this.controlIds = data.controlIds; + } + + addVisibilityRule(): void { + this.visibilityRules.push(new VisibilityRule('', '=', '')); + } + + deleteVisibilityRule(index: number): void { + this.visibilityRules.splice(index, 1); + } +} diff --git a/projects/editor/src/app/components/new-ui-element-panel/state-variables-button/show-state-variables-button.component.html b/projects/editor/src/app/components/new-ui-element-panel/state-variables-button/show-state-variables-button.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4639a48d74b27aa5404b4371d6a8743192cac5a2 --- /dev/null +++ b/projects/editor/src/app/components/new-ui-element-panel/state-variables-button/show-state-variables-button.component.html @@ -0,0 +1,7 @@ +<button mat-flat-button + class="show-state-variables-button" + color="accent" + (click)="showStateVariablesDialog()"> + <mat-icon>integration_instructions</mat-icon> + <span>State Variables</span> +</button> diff --git a/projects/editor/src/app/components/new-ui-element-panel/state-variables-button/show-state-variables-button.component.scss b/projects/editor/src/app/components/new-ui-element-panel/state-variables-button/show-state-variables-button.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..dd235344659b5c044e99e1c60edc946f45106d00 --- /dev/null +++ b/projects/editor/src/app/components/new-ui-element-panel/state-variables-button/show-state-variables-button.component.scss @@ -0,0 +1,4 @@ +.show-state-variables-button { + width: 100%; + +} diff --git a/projects/editor/src/app/components/new-ui-element-panel/state-variables-button/show-state-variables-button.component.ts b/projects/editor/src/app/components/new-ui-element-panel/state-variables-button/show-state-variables-button.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d20bc5d16482b89ab839c385524a4e437bc48768 --- /dev/null +++ b/projects/editor/src/app/components/new-ui-element-panel/state-variables-button/show-state-variables-button.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { DialogService } from 'editor/src/app/services/dialog.service'; +import { UnitService } from 'editor/src/app/services/unit.service'; + +@Component({ + selector: 'aspect-show-state-variables-button', + templateUrl: './show-state-variables-button.component.html', + styleUrls: ['./show-state-variables-button.component.scss'] +}) +export class ShowStateVariablesButtonComponent { + constructor(private dialogService: DialogService, + private unitService: UnitService) { } + + showStateVariablesDialog() { + this.dialogService.showStateVariablesDialog(this.unitService.unit.stateVariables) + .subscribe(stateVariables => { + if (stateVariables) { + this.unitService.updateStateVariables(stateVariables); + } + }); + } +} 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 580c8983a0a1c923609a9f377c7ad36e4ee5309a..7e83e557d951cf0ff671c31f5c4e3aa6a140004d 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 @@ -138,4 +138,7 @@ {{'toolbox.geometry' | translate }} </button> </mat-expansion-panel> + <aspect-show-state-variables-button></aspect-show-state-variables-button> </mat-accordion> + + diff --git a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts index 92270fc354fe78a3b0249daab8c99122318c9052..cf9ec2ad4a997751030d02d6e65b0433bee82ca2 100644 --- a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts +++ b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.ts @@ -8,11 +8,12 @@ import { TranslateService } from '@ngx-translate/core'; import { MessageService } from 'common/services/message.service'; import { UIElement } from 'common/models/elements/element'; import { LikertRowElement } from 'common/models/elements/compound-elements/likert/likert-row'; +import { TextLabel } from 'common/models/elements/label-interfaces'; +import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; +import { StateVariable } from 'common/models/state-variable'; import { UnitService } from '../../services/unit.service'; import { SelectionService } from '../../services/selection.service'; import { CanvasElementOverlay } from '../canvas/overlays/canvas-element-overlay'; -import { TextLabel } from 'common/models/elements/label-interfaces'; -import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; export type CombinedProperties = UIElement & { idList?: string[] }; @@ -29,7 +30,6 @@ export class ElementPropertiesPanelComponent implements OnInit, OnDestroy { interactionEnabled = false; interactionIndeterminate = false; - constructor(private selectionService: SelectionService, public unitService: UnitService, private messageService: MessageService, public sanitizer: DomSanitizer, @@ -104,7 +104,7 @@ export class ElementPropertiesPanelComponent implements OnInit, OnDestroy { } updateModel(property: string, - value: string | number | boolean | string[] | boolean[] | Hotspot[] | + value: string | number | boolean | string[] | boolean[] | Hotspot[] | StateVariable | TextLabel | TextLabel[] | LikertRowElement[] | null, isInputValid: boolean | null = true): void { if (isInputValid) { diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts index e4a173ccc26f0a7b20415806869995f31f213eef..565cf19ff7b7447a007139e566b55b862ed260a6 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/element-model-properties.component.ts @@ -2,21 +2,18 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { moveItemInArray } from '@angular/cdk/drag-drop'; -import { - InputElementValue, UIElement -} from 'common/models/elements/element'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { InputElementValue, UIElement } from 'common/models/elements/element'; import { LikertRowElement } from 'common/models/elements/compound-elements/likert/likert-row'; import { FileService } from 'common/services/file.service'; import { CombinedProperties } from 'editor/src/app/components/properties-panel/element-properties-panel.component'; import { firstValueFrom } from 'rxjs'; +import { TextImageLabel, TextLabel } from 'common/models/elements/label-interfaces'; +import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; +import { StateVariable } from 'common/models/state-variable'; import { UnitService } from '../../../services/unit.service'; import { SelectionService } from '../../../services/selection.service'; import { DialogService } from '../../../services/dialog.service'; -import { TextImageLabel, TextLabel } from 'common/models/elements/label-interfaces'; -import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; @Component({ selector: 'aspect-element-model-properties-component', @@ -28,14 +25,13 @@ export class ElementModelPropertiesComponent { @Input() selectedElements: UIElement[] = []; @Output() updateModel = new EventEmitter<{ property: string; - value: InputElementValue | TextImageLabel[] | LikertRowElement[] | TextLabel[] | Hotspot[] + value: InputElementValue | TextImageLabel[] | LikertRowElement[] | TextLabel[] | Hotspot[] | StateVariable isInputValid?: boolean | null }>(); constructor(public unitService: UnitService, public selectionService: SelectionService, - public dialogService: DialogService, - public sanitizer: DomSanitizer) { } + public dialogService: DialogService) { } addListValue(property: string, value: string): void { this.updateModel.emit({ diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/button-action-param-state-variable.component.html b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/button-action-param-state-variable.component.html new file mode 100644 index 0000000000000000000000000000000000000000..46d785a6a551a2171f8ca2ac1bd4082ae11e9ce4 --- /dev/null +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/button-action-param-state-variable.component.html @@ -0,0 +1,21 @@ +<div class="fx-column-start-stretch"> + <mat-form-field> + <mat-label>Id</mat-label> + <mat-select [(ngModel)]="stateVariable.id" + (ngModelChange)="stateVariableChange.emit(stateVariable)"> + <mat-option *ngFor="let id of stateVariableIds" + [value]="id"> + {{id}} + </mat-option> + </mat-select> + </mat-form-field> + + <mat-form-field> + <mat-label>Wert</mat-label> + <input matInput + placeholder="Initialwert" + [(ngModel)]="stateVariable.value" + (ngModelChange)="stateVariableChange.emit(stateVariable)"> + </mat-form-field> +</div> + diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/button-action-param-state-variable.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/button-action-param-state-variable.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e755bd51ad496a3b05ab5e2fae8f6bb8fd6b0574 --- /dev/null +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/button-action-param-state-variable.component.ts @@ -0,0 +1,14 @@ +import { + Component, EventEmitter, Input, Output +} from '@angular/core'; +import { StateVariable } from 'common/models/state-variable'; + +@Component({ + selector: 'aspect-button-action-param-state-variable', + templateUrl: './button-action-param-state-variable.component.html' +}) +export class ButtonActionParamStateVariableComponent { + @Input() stateVariable!: StateVariable; + @Input() stateVariableIds!: string[]; + @Output() stateVariableChange = new EventEmitter<StateVariable>(); +} 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/button-properties.component.ts similarity index 59% rename from projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties.component.ts rename to projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/button-properties.component.ts index ca75c9d97b0dfacd9c0b7edd32b4c0e6221a3e93..d727afaa555840bdd90d86bc0552b3f2cf3cb80b 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/button-properties.component.ts @@ -6,8 +6,9 @@ import { FileService } from 'common/services/file.service'; import { UIElement } from 'common/models/elements/element'; import { TextComponent } from 'common/components/text/text.component'; import { Page } from 'common/models/page'; -import { UnitService } from '../../../../services/unit.service'; -import { SelectionService } from '../../../../services/selection.service'; +import { StateVariable } from 'common/models/state-variable'; +import { UnitService } from '../../../../../services/unit.service'; +import { SelectionService } from '../../../../../services/selection.service'; @Component({ selector: 'aspect-button-properties', @@ -28,46 +29,59 @@ import { SelectionService } from '../../../../services/selection.service'; <mat-option [value]="null"> {{ 'propertiesPanel.none' | translate }} </mat-option> - <mat-option *ngFor="let option of ['unitNav', 'pageNav', 'highlightText']" + <mat-option *ngFor="let option of ['unitNav', 'pageNav', 'highlightText', 'stateVariableChange']" [value]="option"> {{ 'propertiesPanel.' + option | translate }} </mat-option> </mat-select> </mat-form-field> - <mat-form-field appearance="fill"> + <ng-container *ngIf="combinedProperties.action === 'stateVariableChange'"> + <aspect-button-action-param-state-variable + *ngIf="unitService.unit.stateVariables.length" + [stateVariableIds]="unitService.unit.stateVariables | getStateVariableIds" + [stateVariable]="combinedProperties.actionParam ? + $any(combinedProperties.actionParam) : + {id: unitService.unit.stateVariables[0].id, value: unitService.unit.stateVariables[0].value}" + (stateVariableChange)="updateModel.emit({ property: 'actionParam', value: $event })"> + </aspect-button-action-param-state-variable> + <p *ngIf="!unitService.unit.stateVariables.length">Bitte zuerst Player-Variablen anlegen</p> + </ng-container> + + <mat-form-field *ngIf="combinedProperties.action !== 'stateVariableChange'" + appearance="fill"> <mat-label>{{'propertiesPanel.actionParam' | translate }}</mat-label> - <mat-select [disabled]="combinedProperties.action === null" - [value]="combinedProperties.actionParam" - [matTooltipDisabled]="combinedProperties.action !== 'pageNav'" - [matTooltip]="'propertiesPanel.pageNavSelectionHint' | translate" - (selectionChange)="updateModel.emit({ property: 'actionParam', value: $event.value })"> - - <ng-container *ngIf="combinedProperties.action === 'pageNav'"> - <ng-container *ngFor="let page of (unitService.unit.pages | scrollPages); index as i"> - <mat-option *ngIf="(unitService.unit.pages | scrollPageIndex: selectionService.selectedPageIndex) !== i" - [value]="i"> - {{'page' | translate}} {{i + 1}} - </mat-option> - </ng-container> + <mat-select [disabled]="combinedProperties.action === null" + [value]="combinedProperties.actionParam" + [matTooltipDisabled]="combinedProperties.action !== 'pageNav'" + [matTooltip]="'propertiesPanel.pageNavSelectionHint' | translate" + (selectionChange)="updateModel.emit({ property: 'actionParam', value: $event.value })"> + + <ng-container *ngIf="combinedProperties.action === 'pageNav'"> + <ng-container *ngFor="let page of (unitService.unit.pages | scrollPages); index as i"> + <mat-option *ngIf="(unitService.unit.pages | scrollPageIndex: selectionService.selectedPageIndex) !== i" + [value]="i"> + {{'page' | translate}} {{i + 1}} + </mat-option> </ng-container> + </ng-container> - <ng-container *ngIf="combinedProperties.action === 'unitNav'"> - <mat-option *ngFor="let option of [undefined, 'previous', 'next', 'first', 'last', 'end']" - [value]="option"> - {{ 'propertiesPanel.' + option | translate }} - </mat-option> - </ng-container> + <ng-container *ngIf="combinedProperties.action === 'unitNav'"> + <mat-option *ngFor="let option of [undefined, 'previous', 'next', 'first', 'last', 'end']" + [value]="option"> + {{ 'propertiesPanel.' + option | translate }} + </mat-option> + </ng-container> - <ng-container *ngIf="combinedProperties.action === 'highlightText'"> - <mat-option *ngFor="let option of (textComponents | getAnchorIds) " - [value]="option"> - {{ option }} - </mat-option> - </ng-container> + <ng-container *ngIf="combinedProperties.action === 'highlightText'"> + <mat-option *ngFor="let option of (textComponents | getAnchorIds) " + [value]="option"> + {{ option }} + </mat-option> + </ng-container> - </mat-select> + </mat-select> </mat-form-field> <div class="image-panel" (mouseenter)="hoveringImage = true" (mouseleave)="hoveringImage = false"> @@ -86,7 +100,9 @@ import { SelectionService } from '../../../../services/selection.service'; export class ButtonPropertiesComponent { @Input() combinedProperties!: UIElement; @Output() updateModel = - new EventEmitter<{ property: string; value: string | number | boolean | null, isInputValid?: boolean | null }>(); + new EventEmitter<{ + property: string; value: string | number | boolean | StateVariable | null, isInputValid?: boolean | null + }>(); hoveringImage = false; textComponents: { [id: string]: TextComponent } = {}; diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/get-state-variable-ids.pipe.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/get-state-variable-ids.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..57b41c24eb9a3f9f8680aac5f1665aa832f4ad49 --- /dev/null +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/button-properties/get-state-variable-ids.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { StateVariable } from 'common/models/state-variable'; + +@Pipe({ + name: 'getStateVariableIds' +}) +export class GetStateVariableIdsPipe implements PipeTransform { + transform(stateVariables: StateVariable[]): string[] { + return stateVariables.map(stateVariable => stateVariable.id); + } +} diff --git a/projects/editor/src/app/services/dialog.service.ts b/projects/editor/src/app/services/dialog.service.ts index f682020ccd1c0bd6cb9fe5289f90be07b7082c29..b18071400f23a627244b39fccf1946c6275cbdd1 100644 --- a/projects/editor/src/app/services/dialog.service.ts +++ b/projects/editor/src/app/services/dialog.service.ts @@ -10,6 +10,17 @@ import { GeogebraAppDefinitionDialogComponent } from 'editor/src/app/components/dialogs/geogebra-app-definition-dialog.component'; import { HotspotEditDialogComponent } from 'editor/src/app/components/dialogs/hotspot-edit-dialog.component'; +import { PlayerProperties } from 'common/models/elements/property-group-interfaces'; +import { DragNDropValueObject, Label, TextImageLabel } from 'common/models/elements/label-interfaces'; +import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; +import { + StateVariablesDialogComponent +} from 'editor/src/app/components/dialogs/state-variables-dialog/state-variables-dialog.component'; +import { VisibilityRule } from 'common/models/visibility-rule'; +import { + VisibilityRulesDialogComponent +} from 'editor/src/app/components/dialogs/visibility-rules-dialog/visibility-rules-dialog.component'; +import { StateVariable } from 'common/models/state-variable'; import { ConfirmationDialogComponent } from '../components/dialogs/confirmation-dialog.component'; import { TextEditDialogComponent } from '../components/dialogs/text-edit-dialog.component'; import { TextEditMultilineDialogComponent } from '../components/dialogs/text-edit-multiline-dialog.component'; @@ -17,9 +28,6 @@ import { RichTextEditDialogComponent } from '../components/dialogs/rich-text-edi import { PlayerEditDialogComponent } from '../components/dialogs/player-edit-dialog.component'; import { LikertRowEditDialogComponent } from '../components/dialogs/likert-row-edit-dialog.component'; import { DropListOptionEditDialogComponent } from '../components/dialogs/drop-list-option-edit-dialog.component'; -import { PlayerProperties } from 'common/models/elements/property-group-interfaces'; -import { DragNDropValueObject, Label, TextImageLabel } from 'common/models/elements/label-interfaces'; -import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; @Injectable({ providedIn: 'root' @@ -124,4 +132,23 @@ export class DialogService { }); return dialogRef.afterClosed(); } + + showVisibilityRulesDialog(visibilityRules: VisibilityRule[], + controlIds: string[], + activeAfterIdDelay: number + ): Observable<{ visibilityRules: VisibilityRule[], activeAfterIdDelay: number }> { + const dialogRef = this.dialog.open(VisibilityRulesDialogComponent, { + data: { visibilityRules, controlIds, activeAfterIdDelay }, + autoFocus: false + }); + return dialogRef.afterClosed(); + } + + showStateVariablesDialog(stateVariables: StateVariable[]): Observable<StateVariable[]> { + const dialogRef = this.dialog.open(StateVariablesDialogComponent, { + data: { stateVariables: stateVariables }, + autoFocus: false + }); + return dialogRef.afterClosed(); + } } diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts index 0cab8a17790e1e5297a8297a23ac00da19719cbe..e4e5d13d1feb8e96a65224cf81678461510091a7 100644 --- a/projects/editor/src/app/services/unit.service.ts +++ b/projects/editor/src/app/services/unit.service.ts @@ -22,13 +22,15 @@ import { Page } from 'common/models/page'; import { Section } from 'common/models/section'; import { ElementFactory } from 'common/util/element.factory'; import { ReferenceManager } from 'editor/src/app/services/reference-manager'; -import { DialogService } from './dialog.service'; -import { VeronaAPIService } from './verona-api.service'; -import { SelectionService } from './selection.service'; -import { IDService } from './id.service'; import { PlayerProperties, PositionProperties } from 'common/models/elements/property-group-interfaces'; import { DragNDropValueObject, TextLabel } from 'common/models/elements/label-interfaces'; import { Hotspot } from 'common/models/elements/input-elements/hotspot-image'; +import { VisibilityRule } from 'common/models/visibility-rule'; +import { StateVariable } from 'common/models/state-variable'; +import { IDService } from './id.service'; +import { SelectionService } from './selection.service'; +import { VeronaAPIService } from './verona-api.service'; +import { DialogService } from './dialog.service'; @Injectable({ providedIn: 'root' @@ -265,7 +267,7 @@ export class UnitService { return newElement; } - updateSectionProperty(section: Section, property: string, value: string | number | boolean | { value: number; unit: string }[]): void { + updateSectionProperty(section: Section, property: string, value: string | number | boolean | VisibilityRule[] | { value: number; unit: string }[]): void { if (property === 'dynamicPositioning') { section.dynamicPositioning = value as boolean; section.elements.forEach((element: UIElement) => { @@ -280,7 +282,7 @@ export class UnitService { updateElementsProperty(elements: UIElement[], property: string, - value: InputElementValue | LikertRowElement[] | Hotspot[] | + value: InputElementValue | LikertRowElement[] | Hotspot[] | StateVariable | TextLabel | TextLabel[] | ClozeDocument | null): void { console.log('updateElementProperty', elements, property, value); elements.forEach(element => { @@ -511,4 +513,9 @@ export class UnitService { this.deleteSection(this.unit.pages[pageIndex].sections[sectionIndex]); this.addSection(this.unit.pages[pageIndex], newSection); } + + updateStateVariables(stateVariables: StateVariable[]): void { + this.unit.stateVariables = stateVariables; + this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); + } } diff --git a/projects/player/modules/verona/models/verona.ts b/projects/player/modules/verona/models/verona.ts index e6bac6e288a5e3231ae6ace2a4ee249d5af58749..19def54f5700f5e37d621a02896957596761a04e 100644 --- a/projects/player/modules/verona/models/verona.ts +++ b/projects/player/modules/verona/models/verona.ts @@ -5,8 +5,8 @@ export type RunningState = 'running' | 'stopped'; export type Progress = 'none' | 'some' | 'complete'; export type PagingMode = 'separate' | 'concat-scroll' | 'concat-scroll-snap'; export type StateReportPolicy = 'none' | 'eager' | 'on-demand'; -export type ElementCodeStatus = 'NOT_REACHED' | 'DISPLAYED' | 'VALUE_CHANGED'; -export enum ElementCodeStatusValue { NOT_REACHED = 0, DISPLAYED = 1, VALUE_CHANGED = 2} +export type ElementCodeStatus = 'DERIVED' | 'NOT_REACHED' | 'DISPLAYED' | 'VALUE_CHANGED'; +export enum ElementCodeStatusValue { DERIVED = 0, NOT_REACHED = 1, DISPLAYED = 2, VALUE_CHANGED = 3} export interface StatusChangeElement { id: string; diff --git a/projects/player/src/app/classes/timer-state-variable.spec.ts b/projects/player/src/app/classes/timer-state-variable.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e8af0f5395098136966c2f7232b4b9dea3fa904 --- /dev/null +++ b/projects/player/src/app/classes/timer-state-variable.spec.ts @@ -0,0 +1,7 @@ +import { TimerStateVariable } from './timer-state-variable'; + +describe('State', () => { + it('should create an instance', () => { + expect(new TimerStateVariable('test', 0, 3000)).toBeTruthy(); + }); +}); diff --git a/projects/player/src/app/classes/timer-state-variable.ts b/projects/player/src/app/classes/timer-state-variable.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac75713d67906f0443966f9a089d700656e69722 --- /dev/null +++ b/projects/player/src/app/classes/timer-state-variable.ts @@ -0,0 +1,36 @@ +import { EventEmitter } from '@angular/core'; +import { ValueChangeElement } from 'common/models/elements/element'; + +export class TimerStateVariable { + id: string; + value: number; + duration: number; + elementValueChanged = new EventEmitter<ValueChangeElement>(); + + private interval: number = 0; + + constructor(id: string, value: number, duration: number) { + this.id = id; + this.value = value; + this.duration = duration; + } + + run(): void { + if (!this.interval) { + this.interval = setInterval(() => { + this.value += 1000; + this.elementValueChanged.emit({ id: this.id, value: this.value }); + if (this.value >= this.duration) { + this.stop(); + } + }, 1000); + } + } + + stop(): void { + if (this.interval) { + clearInterval(this.interval); + this.interval = 0; + } + } +} 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 49e8406dcb6f31b1a5362603f7ca1c163d41a49b..644fad4ee3ff2491083b6a3ecf77412292c8454d 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 @@ -8,12 +8,13 @@ import { import { ClozeElement } from 'common/models/elements/compound-elements/cloze/cloze'; import { LikertElement } from 'common/models/elements/compound-elements/likert/likert'; import { - CompoundElement, InputElement, InputElementValue + CompoundElement, InputElement, InputElementValue, ValueChangeElement } from 'common/models/elements/element'; import { ButtonComponent } from 'common/components/button/button.component'; import { VeronaPostService } from 'player/modules/verona/services/verona-post.service'; import { NavigationService } from 'player/src/app/services/navigation.service'; import { AnchorService } from 'player/src/app/services/anchor.service'; +import { UnitNavParam } from 'common/models/elements/button/button'; import { UnitStateService } from '../../../services/unit-state.service'; import { ElementModelElementCodeMappingService } from '../../../services/element-model-element-code-mapping.service'; import { ValidationService } from '../../../services/validation.service'; @@ -22,8 +23,6 @@ import { ElementFormGroupDirective } from '../../../directives/element-form-grou import { KeyboardService } from '../../../services/keyboard.service'; import { DeviceService } from '../../../services/device.service'; -import { UnitNavParam } from 'common/models/elements/button/button'; - @Component({ selector: 'aspect-compound-group-element', templateUrl: './compound-group-element.component.html', @@ -146,6 +145,9 @@ export class CompoundGroupElementComponent extends ElementFormGroupDirective imp case 'highlightText': this.anchorService.toggleAnchor(navigationEvent.param as string); break; + case 'stateVariableChange': + this.unitStateService.changeElementCodeValue(navigationEvent.param as ValueChangeElement); + break; default: } }); 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 f5d16ab759e4f2329fa3edcf97864e9aa1e40455..9fde6fd9d3ceff27699807f2b8b1cbd3ddfcab42 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 @@ -5,7 +5,7 @@ import { ElementComponent } from 'common/directives/element-component.directive' import { ButtonElement, ButtonEvent, UnitNavParam } from 'common/models/elements/button/button'; import { FrameElement } from 'common/models/elements/frame/frame'; import { ImageElement } from 'common/models/elements/media-elements/image'; -import { InputElementValue } from 'common/models/elements/element'; +import { InputElementValue, ValueChangeElement } from 'common/models/elements/element'; import { VeronaPostService } from 'player/modules/verona/services/verona-post.service'; import { AnchorService } from 'player/src/app/services/anchor.service'; import { NavigationService } from '../../../services/navigation.service'; @@ -59,6 +59,9 @@ export class InteractiveGroupElementComponent extends ElementGroupDirective impl case 'highlightText': this.anchorService.toggleAnchor(navigationEvent.param as string); break; + case 'stateVariableChange': + this.unitStateService.changeElementCodeValue(navigationEvent.param as ValueChangeElement); + break; default: } } diff --git a/projects/player/src/app/components/unit/unit.component.ts b/projects/player/src/app/components/unit/unit.component.ts index 209a6b21c10582068d701a5475ccabbb858a67bb..255e0599617281c3fc98b8bd853e9d4e556a5b38 100644 --- a/projects/player/src/app/components/unit/unit.component.ts +++ b/projects/player/src/app/components/unit/unit.component.ts @@ -54,8 +54,7 @@ export class UnitComponent implements OnInit { this.metaDataService.resourceURL = this.playerConfig.directDownloadUrl; this.veronaPostService.sessionID = message.sessionId; this.veronaPostService.stateReportPolicy = message.playerConfig?.stateReportPolicy || 'none'; - this.unitStateService.elementCodes = message.unitState?.dataParts?.elementCodes ? - JSON.parse(message.unitState.dataParts.elementCodes) : []; + this.initUnitStateService(message, unitDefinition); this.elementModelElementCodeMappingService.dragNDropValueObjects = [ ...unitDefinition.getAllElements('drop-list'), ...unitDefinition.getAllElements('drop-list-simple')] @@ -65,6 +64,14 @@ export class UnitComponent implements OnInit { } } + private initUnitStateService(message: VopStartCommand, unitDefinition: Unit): void { + this.unitStateService.elementCodes = message.unitState?.dataParts?.elementCodes ? + JSON.parse(message.unitState.dataParts.elementCodes) : []; + unitDefinition.stateVariables + .map(stateVariable => this.unitStateService + .registerElement(stateVariable.id, stateVariable.value, null, null)); + } + private reset(): void { this.pages = []; this.playerConfig = {}; diff --git a/projects/player/src/app/directives/section-visibility-handling.directive.ts b/projects/player/src/app/directives/section-visibility-handling.directive.ts index 0e764708500f9594b161442433ce245ea525e3e3..c058a7d228ec1059f5805d27008dd785d0701903 100644 --- a/projects/player/src/app/directives/section-visibility-handling.directive.ts +++ b/projects/player/src/app/directives/section-visibility-handling.directive.ts @@ -1,14 +1,18 @@ -import { Directive, ElementRef, Input } from '@angular/core'; -import { delay, Subject } from 'rxjs'; +import { + Directive, ElementRef, Input, OnInit +} from '@angular/core'; +import { Subject } from 'rxjs'; import { Section } from 'common/models/section'; import { takeUntil } from 'rxjs/operators'; import { ElementCodeStatusValue } from 'player/modules/verona/models/verona'; import { UnitStateService } from 'player/src/app/services/unit-state.service'; +import { TimerStateVariable } from 'player/src/app/classes/timer-state-variable'; +import { ValueChangeElement } from 'common/models/elements/element'; @Directive({ selector: '[aspectSectionVisibilityHandling]' }) -export class SectionVisibilityHandlingDirective { +export class SectionVisibilityHandlingDirective implements OnInit { @Input() mediaStatusChanged!: Subject<string>; @Input() section!: Section; @Input() pageSections!: Section[]; @@ -22,18 +26,70 @@ export class SectionVisibilityHandlingDirective { ) {} ngOnInit(): void { - this.setVisibility(!this.section.activeAfterID); - if (!this.isVisible) { - this.mediaStatusChanged - .pipe( - takeUntil(this.ngUnsubscribe), - delay(this.section.activeAfterIdDelay)) - .subscribe((id: string): void => { - this.ngUnsubscribe.next(); - this.ngUnsubscribe.complete(); - this.setActiveAfterID(id); + if (this.section.visibilityRules.length) { + this.unitStateService.elementCodeChanged + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe(code => { + this.displaySection(); }); } + + // this.setVisibility(!this.section.activeAfterID); + // if (!this.isVisible) { + // this.mediaStatusChanged + // .pipe( + // takeUntil(this.ngUnsubscribe), + // delay(this.section.activeAfterIdDelay)) + // .subscribe((id: string): void => { + // this.ngUnsubscribe.next(); + // this.ngUnsubscribe.complete(); + // this.setActiveAfterID(id); + // }); + // } + } + + displaySection(): void { + if (this.isSectionVisible()) { + if (this.section.activeAfterIdDelay) { + // sollte die gleiche id wie die dazugehörige Rule benutzen + if (!this.unitStateService.getElementCodeById('test-3000')) { + const st = new TimerStateVariable('test-3000', 0, this.section.activeAfterIdDelay); + this.unitStateService.registerElement(st.id, st.value, null, null); + st.run(); + st.elementValueChanged.subscribe((value: ValueChangeElement) => { + this.unitStateService.changeElementCodeValue(value); + }); + } + if ((this.unitStateService.getElementCodeById('test-3000')?.value as number) >= this.section.activeAfterIdDelay) { + this.elementRef.nativeElement.style.display = 'block'; + } else { + this.elementRef.nativeElement.style.display = 'none'; + } + } + } else { + this.elementRef.nativeElement.style.display = 'none'; + } + } + + isSectionVisible(): boolean { + return this.section.visibilityRules.some(rule => { + console.log(this.section.visibilityRules, rule.id, this.unitStateService.getElementCodeById(rule.id)); + if (this.unitStateService.getElementCodeById(rule.id)) { + switch (rule.operator) { + case '=': + return this.unitStateService.getElementCodeById(rule.id)?.value === rule.value; + case '!=': + return this.unitStateService.getElementCodeById(rule.id)?.value !== rule.value; + case '>': + return Number(this.unitStateService.getElementCodeById(rule.id)?.value) > Number(rule.value); + case '<': + return Number(this.unitStateService.getElementCodeById(rule.id)?.value) < Number(rule.value); + default: + return false; + } + } + return false; + }); } private setVisibility(isVisible: boolean): void { diff --git a/projects/player/src/app/services/unit-state.service.ts b/projects/player/src/app/services/unit-state.service.ts index 96ff59d09457ea1b89fad8f4b94983fbd8ecc01a..3303c0fb46443d38b8bd24f6d3ec45a41c94dbe9 100644 --- a/projects/player/src/app/services/unit-state.service.ts +++ b/projects/player/src/app/services/unit-state.service.ts @@ -53,16 +53,25 @@ export class UnitStateService { registerElement(elementId: string, elementValue: InputElementValue, - domElement: Element, - pageIndex: number): void { - this.elementIdPageIndexMap[elementId] = pageIndex; + domElement: Element | null, + pageIndex: number | null): void { + if (pageIndex !== null) { + this.elementIdPageIndexMap[elementId] = pageIndex; + } this.addElementCode(elementId, elementValue, domElement); } changeElementCodeValue(elementValue: ValueChangeElement): void { - LogService.info(`player: changeElementValue ${elementValue.id}: ${elementValue.value}`); + LogService.debug(`player: changeElementValue ${elementValue.id}: ${elementValue.value}`); this.setElementCodeValue(elementValue.id, elementValue.value); - this.setElementCodeStatus(elementValue.id, 'VALUE_CHANGED'); + const unitStateElementCode = this.getElementCodeById(elementValue.id); + if (unitStateElementCode) { + if (unitStateElementCode.status !== 'DERIVED') { + this.setElementCodeStatus(elementValue.id, 'VALUE_CHANGED'); + } else { + this._elementCodeChanged.next(unitStateElementCode); + } + } } changeElementCodeStatus(elementStatus: StatusChangeElement): void { @@ -139,18 +148,20 @@ export class UnitStateService { } } - private addElementCode(id: string, value: InputElementValue, domElement: Element): void { + private addElementCode(id: string, value: InputElementValue, domElement: Element | null): void { let unitStateElementCode = this.getElementCodeById(id); if (!unitStateElementCode) { // when reloading a unit, elementCodes are already pushed - unitStateElementCode = { id: id, value: value, status: 'NOT_REACHED' }; + const status = domElement ? 'NOT_REACHED' : 'DERIVED'; + unitStateElementCode = { id, value, status }; this.elementCodes.push(unitStateElementCode); this._elementCodeChanged.next(unitStateElementCode); - } else if (Object.keys(this.elementIdPageIndexMap).length === this.elementCodes.length) { + } else if (Object.keys(this.elementIdPageIndexMap).length === this.elementCodes + .filter(e => e.status !== 'DERIVED').length) { // if all elements are registered, we can rebuild the presentedPages array this.buildPresentedPages(); } - if (unitStateElementCode.status === 'NOT_REACHED') { + if (domElement && unitStateElementCode.status === 'NOT_REACHED') { this.addIntersectionDetection(id, domElement); } }