diff --git a/projects/common/form.service.ts b/projects/common/form.service.ts index 190adfa66d2423684eb6f215d55e450529d64a76..7a824b59a11db6a7ef1c975a36e6bf53868a5f8b 100644 --- a/projects/common/form.service.ts +++ b/projects/common/form.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { - FormControlElement, FormControlValidators, ChildFormGroup + FormControlElement, FormControlValidators, ChildFormGroup, ValueChangeElement } from './form'; @Injectable({ providedIn: 'root' }) export class FormService { + private _elementValueChanged = new Subject<ValueChangeElement>(); private _controlAdded = new Subject<FormControlElement>(); private _groupAdded = new Subject<ChildFormGroup>(); private _validatorsAdded = new Subject<FormControlValidators>(); @@ -29,6 +30,14 @@ export class FormService { return this._presentedPageAdded.asObservable(); } + get elementValueChanged(): Observable<ValueChangeElement> { + return this._elementValueChanged.asObservable(); + } + + changeElementValue(elementValues: ValueChangeElement): void { + this._elementValueChanged.next(elementValues); + } + registerFormControl(control: FormControlElement): void { this._controlAdded.next(control); } diff --git a/projects/common/form.ts b/projects/common/form.ts index 61e8238f9717d81937cfa38658ee7c7a3a567df6..f2f3a93f5263417d0306096bfd7969cd60a878ea 100644 --- a/projects/common/form.ts +++ b/projects/common/form.ts @@ -23,12 +23,3 @@ export interface ChildFormGroup { parentArray: 'pages' | 'sections' | 'elements'; parentArrayIndex: number; } - -export interface FormSection { - elements: Record<string, string | number | boolean | undefined>[]; -} - -export interface FormPage { - sections: FormSection[]; - id?: string; -} diff --git a/projects/player/src/app/app.component.ts b/projects/player/src/app/app.component.ts index 66935f791d15cb2c3eefeb2624d6e309cf8fd12b..10fae95c7dd5f1ff2fd2a6d494095d15985935d4 100644 --- a/projects/player/src/app/app.component.ts +++ b/projects/player/src/app/app.component.ts @@ -2,14 +2,13 @@ import { Component, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { - Unit, UnitPage, UnitPageSection, UnitUIElement + Unit, UnitPage } from '../../../common/unit'; import { VeronaSubscriptionService } from './services/verona-subscription.service'; import { VeronaPostService } from './services/verona-post.service'; import { NativeEventService } from './services/native-event.service'; import { MetaDataService } from './services/meta-data.service'; -import { PlayerConfig, UnitState, VopStartCommand } from './models/verona'; -import { FormPage } from '../../../common/form'; +import { PlayerConfig, UnitStateElementCode, VopStartCommand } from './models/verona'; import { AlertDialogComponent } from './components/alert-dialog/alert-dialog.component'; import { KeyboardService } from './services/keyboard.service'; @@ -17,14 +16,16 @@ import { KeyboardService } from './services/keyboard.service'; selector: 'player-aspect', template: ` <app-unit-state *ngIf="playerConfig && pages?.length" - [pages]=pages - [playerConfig]=playerConfig> + [pages]="pages" + [playerConfig]="playerConfig" + [unitStateElementCodes]="unitStateElementCodes"> </app-unit-state> ` }) export class AppComponent implements OnInit { pages!: UnitPage[]; playerConfig!: PlayerConfig | undefined; + unitStateElementCodes!: UnitStateElementCode[]; constructor(private translateService: TranslateService, private veronaSubscriptionService: VeronaSubscriptionService, @@ -49,28 +50,6 @@ export class AppComponent implements OnInit { .subscribe((focused: boolean): void => this.onFocus(focused)); } - private initUnitPages(pages: UnitPage[], unitState: UnitState | undefined): void { - const storedPages: FormPage[] = unitState?.dataParts?.pages ? - JSON.parse(unitState.dataParts.pages) : []; - if (storedPages.length > 0) { - if (this.metaDataService.verifyUnitStateDataType(unitState?.unitStateDataType)) { - this.pages = this.addStoredValues(pages, storedPages); - } else { - // eslint-disable-next-line no-console - this.dialog.open(AlertDialogComponent, { - data: { - title: this.translateService.instant('dialogTitle.wrongUnitStateDataType'), - content: this.translateService.instant('dialogContent.wrongUnitStateDataType', - { version: this.metaDataService.playerMetadata.supportedUnitStateDataTypes }) - } - }); - this.pages = pages; - } - } else { - this.pages = pages; - } - } - private onStart(message: VopStartCommand): void { this.reset(); setTimeout(() => { @@ -81,7 +60,9 @@ export class AppComponent implements OnInit { this.playerConfig = message.playerConfig || {}; this.veronaPostService.sessionId = message.sessionId; this.veronaPostService.stateReportPolicy = message.playerConfig?.stateReportPolicy || 'none'; - this.initUnitPages(unitDefinition.pages, message.unitState); + this.pages = unitDefinition.pages; + this.unitStateElementCodes = message.unitState?.dataParts?.elementCodes ? + JSON.parse(message.unitState.dataParts.elementCodes) : []; this.keyboardService.useKeyboard(false); } else { this.dialog.open(AlertDialogComponent, { @@ -95,34 +76,6 @@ export class AppComponent implements OnInit { }); } - // structure of a unitPage: {sections: [{elements: [{id: ..., value: ...}]}]} - // structure of a storedPage: {sections: [{elements: [{[id]: [value]}]}]} - // the array of unitPages contains all pages, all sections and all elements - // the array of storedPages contains all pages, all sections but only form elements - // to add the values of the elements of storedPages to the elements of unitPages - // we need to find the elements in storedPages that match the elements of unitPages - private addStoredValues = (unitPages: UnitPage[], storedPages: FormPage[]): UnitPage[] => unitPages - .map((page: UnitPage, pageIndex: number): UnitPage => ({ - ...page, - sections: page.sections.map((section: UnitPageSection, sectionIndex: number): UnitPageSection => ( - { - ...section, - elements: section.elements - .map((element: UnitUIElement): UnitUIElement => { - const storedValueElement: Record<string, string | number | boolean | undefined> = - storedPages?.[pageIndex]?.sections?.[sectionIndex]?.elements?.find( - (storedElement: Record<string, string | number | boolean | undefined>) => Object - .keys(storedElement)[0] === element.id - ) || {}; - const value = storedValueElement[Object.keys(storedValueElement)[0]]; - return { - ...element, - ...{ value: (value !== undefined && value !== null) ? value : element.value } - }; - }) - })) - })); - private onFocus(focused: boolean): void { // eslint-disable-next-line no-console console.log('player: onFocus', focused); @@ -134,5 +87,6 @@ export class AppComponent implements OnInit { console.log('player: reset'); this.pages = []; this.playerConfig = {}; + this.unitStateElementCodes = []; } } diff --git a/projects/player/src/app/components/element/element.component.ts b/projects/player/src/app/components/element/element.component.ts index 63716714211874267354bf94b57cf20fcc9d6227..4244eceffe215231de0a911ffa1f1632105ee298 100644 --- a/projects/player/src/app/components/element/element.component.ts +++ b/projects/player/src/app/components/element/element.component.ts @@ -11,6 +11,8 @@ import { KeyboardService } from '../../services/keyboard.service'; import { TextFieldComponent } from '../../../../../common/element-components/text-field.component'; import { TextAreaComponent } from '../../../../../common/element-components/text-area.component'; import { FormService } from '../../../../../common/form.service'; +import { ValueChangeElement } from '../../../../../common/form'; +import { UnitStateElementCode } from '../../models/verona'; @Component({ selector: 'app-element', @@ -21,6 +23,7 @@ export class ElementComponent implements OnInit { @Input() elementModel!: UnitUIElement; @Input() parentForm!: FormGroup; @Input() parentArrayIndex!: number; + @Input() unitStateElementCodes!: UnitStateElementCode[]; private isInputElement!: boolean; private focussedInputSubscription!: Subscription; @@ -38,14 +41,27 @@ export class ElementComponent implements OnInit { ngOnInit(): void { const elementComponentFactory = ComponentUtils.getComponentFactory(this.elementModel.type, this.componentFactoryResolver); - const elementComponent = this.elementComponentContainer.createComponent(elementComponentFactory).instance; elementComponent.elementModel = this.elementModel; + this.isInputElement = Object.prototype.hasOwnProperty.call(this.elementModel, 'required'); if (this.isInputElement) { + const unitStateElementCode = this.unitStateElementCodes + .find((element: UnitStateElementCode): boolean => element.id === this.elementModel.id); + if (unitStateElementCode) { + elementComponent.elementModel.value = unitStateElementCode.value; + } + const elementForm = this.formBuilder.group({}); elementComponent.parentForm = elementForm; this.registerFormGroup(elementForm); + + elementComponent.formValueChanged + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe((changeElement: ValueChangeElement) => { + this.formService.changeElementValue(changeElement); + }); + if (this.keyboardService.isActive && (this.elementModel.type === 'text-field' || this.elementModel.type === 'text-area')) { this.initEventsForKeyboard(elementComponent); diff --git a/projects/player/src/app/components/layout/layout.component.html b/projects/player/src/app/components/layout/layout.component.html index 0dbb760f55e1297ed379ac532f61679417c3fad2..c53108fd1df77925eab85676c458efe808d565e2 100644 --- a/projects/player/src/app/components/layout/layout.component.html +++ b/projects/player/src/app/components/layout/layout.component.html @@ -32,7 +32,8 @@ [parentForm]="parentForm" [isLastPage]="false" [pagesContainer]="alwaysVisiblePageContainer" - [page]="alwaysVisiblePage"> + [page]="alwaysVisiblePage" + [unitStateElementCodes]="unitStateElementCodes"> </app-page> </div> </div> @@ -73,7 +74,8 @@ [parentForm]="parentForm" [isLastPage]="last" [pagesContainer]="pagesContainer" - [page]="page"> + [page]="page" + [unitStateElementCodes]="unitStateElementCodes"> </app-page> </div> </mat-tab> @@ -101,7 +103,8 @@ [pagesContainer]="pagesContainer" [page]="page" [isLastPage]="last" - (selectedIndexChange)="onSelectedIndexChange($event)"> + (selectedIndexChange)="onSelectedIndexChange($event)" + [unitStateElementCodes]="unitStateElementCodes"> </app-page> </div> </ng-container> diff --git a/projects/player/src/app/components/layout/layout.component.ts b/projects/player/src/app/components/layout/layout.component.ts index 16ae0c21694095e5c3ba5c8b30ae14acfe9d74c4..07ae0312d291ef3e5a7c43b812bdff24a7679f76 100644 --- a/projects/player/src/app/components/layout/layout.component.ts +++ b/projects/player/src/app/components/layout/layout.component.ts @@ -6,7 +6,7 @@ import { TranslateService } from '@ngx-translate/core'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { UnitPage } from '../../../../../common/unit'; -import { PlayerConfig } from '../../models/verona'; +import { PlayerConfig, UnitStateElementCode } from '../../models/verona'; import { KeyboardService } from '../../services/keyboard.service'; @Component({ @@ -20,6 +20,7 @@ export class LayoutComponent implements OnInit, OnDestroy { @Input() selectedIndex!: number; @Input() selectIndex!: Subject<number>; @Input() playerConfig!: PlayerConfig; + @Input() unitStateElementCodes!: UnitStateElementCode[]; @Output() selectedIndexChange = new EventEmitter<number>(); @Output() validPagesDetermined = new EventEmitter<Record<string, string>[]>(); diff --git a/projects/player/src/app/components/page/page.component.html b/projects/player/src/app/components/page/page.component.html index 8f4f1daed5694071cf9f3a6f2d7c5b335f82fc51..14b6fbd92431b83e06236339aba79f8b1762fb27 100644 --- a/projects/player/src/app/components/page/page.component.html +++ b/projects/player/src/app/components/page/page.component.html @@ -6,6 +6,7 @@ [parentForm]="pageForm" [parentArrayIndex]="i" [section]="section" + [unitStateElementCodes]="unitStateElementCodes" [ngStyle]="{ position: 'relative', display: section.dynamicPositioning ? 'contents' : 'block', diff --git a/projects/player/src/app/components/page/page.component.ts b/projects/player/src/app/components/page/page.component.ts index 02ee57006ea8ab1a162c032cb5d213acbccd490a..cd05e1d0b1fc24278124656572c12f5272de352a 100644 --- a/projects/player/src/app/components/page/page.component.ts +++ b/projects/player/src/app/components/page/page.component.ts @@ -4,6 +4,7 @@ import { import { FormBuilder, FormGroup } from '@angular/forms'; import { UnitPage } from '../../../../../common/unit'; import { FormService } from '../../../../../common/form.service'; +import { UnitStateElementCode } from '../../models/verona'; @Component({ selector: 'app-page', @@ -16,8 +17,11 @@ export class PageComponent implements OnInit { @Input() parentForm!: FormGroup; @Input() parentArrayIndex!: number; @Input() index!: number; + @Input() unitStateElementCodes!: UnitStateElementCode[]; + @Input() pagesContainer!: HTMLElement; @Output() selectedIndexChange = new EventEmitter<number>(); + pageForm!: FormGroup; constructor(private formService: FormService, diff --git a/projects/player/src/app/components/player-state/player-state.component.html b/projects/player/src/app/components/player-state/player-state.component.html index 9335b5f92c052718f508e622d02a15a62887cfd5..224150ef7eaec36d05c57d1f5e7bce30d2c2e057 100644 --- a/projects/player/src/app/components/player-state/player-state.component.html +++ b/projects/player/src/app/components/player-state/player-state.component.html @@ -1,6 +1,7 @@ <div *ngIf="!running" class='stopped-overlay'></div> <app-layout [parentForm]="parentForm" [pages]="pages" + [unitStateElementCodes]="unitStateElementCodes" [playerConfig]="playerConfig" [(selectedIndex)]="currentPlayerPageIndex" [selectIndex]="selectIndex" diff --git a/projects/player/src/app/components/player-state/player-state.component.ts b/projects/player/src/app/components/player-state/player-state.component.ts index aa60748aae3dddb07dbd2bf5abe3d37930ddb914..3011f559ef8f3ea6384884a8cc93f860f4934137 100644 --- a/projects/player/src/app/components/player-state/player-state.component.ts +++ b/projects/player/src/app/components/player-state/player-state.component.ts @@ -7,7 +7,7 @@ import { Subject } from 'rxjs'; import { UnitPage } from '../../../../../common/unit'; import { VeronaSubscriptionService } from '../../services/verona-subscription.service'; import { - PlayerConfig, PlayerState, RunningState, + PlayerConfig, PlayerState, RunningState, UnitStateElementCode, VopContinueCommand, VopGetStateRequest, VopPageNavigationCommand, VopStopCommand } from '../../models/verona'; import { VeronaPostService } from '../../services/verona-post.service'; @@ -21,6 +21,7 @@ export class PlayerStateComponent implements OnInit, OnDestroy { @Input() parentForm!: FormGroup; @Input() pages!: UnitPage[]; @Input() playerConfig!: PlayerConfig; + @Input() unitStateElementCodes!: UnitStateElementCode[]; currentPlayerPageIndex: number = 0; selectIndex: Subject<number> = new Subject(); diff --git a/projects/player/src/app/components/section/section.component.html b/projects/player/src/app/components/section/section.component.html index 8676964b92f7b6201d0a93f1910f3558c9b62e81..e0ad4990dffbf1083e4eb1dd2f309b71690d0f73 100644 --- a/projects/player/src/app/components/section/section.component.html +++ b/projects/player/src/app/components/section/section.component.html @@ -12,7 +12,8 @@ [style.top.px]="element.yPosition" [elementModel]="element" [parentForm]="sectionForm" - [parentArrayIndex]="i"> + [parentArrayIndex]="i" + [unitStateElementCodes]="unitStateElementCodes"> </app-element> </ng-container> </ng-template> @@ -35,7 +36,8 @@ [style.grid-row-end]="element.gridRowEnd" [elementModel]="element" [parentForm]="sectionForm" - [parentArrayIndex]="i"> + [parentArrayIndex]="i" + [unitStateElementCodes]="unitStateElementCodes"> </app-element> </ng-container> </div> diff --git a/projects/player/src/app/components/section/section.component.ts b/projects/player/src/app/components/section/section.component.ts index c8d24a4d781bfaf6ca250053ff240d31047784df..dca4f8d39e1008fb8a73de8282ca5c8368b42b24 100644 --- a/projects/player/src/app/components/section/section.component.ts +++ b/projects/player/src/app/components/section/section.component.ts @@ -4,6 +4,7 @@ import { import { FormBuilder, FormGroup } from '@angular/forms'; import { UnitPageSection } from '../../../../../common/unit'; import { FormService } from '../../../../../common/form.service'; +import { UnitStateElementCode } from '../../models/verona'; @Component({ selector: 'app-section', @@ -13,6 +14,7 @@ export class SectionComponent implements OnInit { @Input() parentForm!: FormGroup; @Input() section!: UnitPageSection; @Input() parentArrayIndex!: number; + @Input() unitStateElementCodes!: UnitStateElementCode[]; sectionForm!: FormGroup; diff --git a/projects/player/src/app/components/unit-state/unit-state.component.html b/projects/player/src/app/components/unit-state/unit-state.component.html index 255692efd1e89845323439e3d3110bd86197a449..212d81f277e19f8edf9df618c98090ed72581baf 100644 --- a/projects/player/src/app/components/unit-state/unit-state.component.html +++ b/projects/player/src/app/components/unit-state/unit-state.component.html @@ -1,6 +1,7 @@ <form [formGroup]="form"> <app-player-state [parentForm]="form" [playerConfig]="playerConfig" - [pages]="pages"> + [pages]="pages" + [unitStateElementCodes]="unitStateElementCodes"> </app-player-state> </form> diff --git a/projects/player/src/app/components/unit-state/unit-state.component.ts b/projects/player/src/app/components/unit-state/unit-state.component.ts index a4e633e8ef505de82b58341deaff7ac1e624aadc..1ffcb1c3a39dc5d8054c15a95b857fd89d2cbb06 100644 --- a/projects/player/src/app/components/unit-state/unit-state.component.ts +++ b/projects/player/src/app/components/unit-state/unit-state.component.ts @@ -14,10 +14,10 @@ import { VeronaPostService } from '../../services/verona-post.service'; import { MessageService } from '../../../../../common/message.service'; import { MetaDataService } from '../../services/meta-data.service'; import { - FormControlElement, FormControlValidators, ChildFormGroup + FormControlElement, FormControlValidators, ChildFormGroup, ValueChangeElement } from '../../../../../common/form'; import { - PlayerConfig, Progress, UnitState, VopNavigationDeniedNotification + PlayerConfig, Progress, UnitState, UnitStateElementCode, UnitStateElementCodeStatus, VopNavigationDeniedNotification } from '../../models/verona'; import { UnitPage } from '../../../../../common/unit'; @@ -28,6 +28,8 @@ import { UnitPage } from '../../../../../common/unit'; export class UnitStateComponent implements OnInit, OnDestroy { @Input() pages: UnitPage[] = []; @Input() playerConfig!: PlayerConfig; + @Input() unitStateElementCodes!: UnitStateElementCode[]; + form!: FormGroup; presentedPages: number[] = []; @@ -60,6 +62,9 @@ export class UnitStateComponent implements OnInit, OnDestroy { this.formService.validatorsAdded .pipe(takeUntil(this.ngUnsubscribe)) .subscribe((validations: FormControlValidators): void => this.setValidators(validations)); + this.formService.elementValueChanged + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe((value: ValueChangeElement): void => this.onElementValueChanges(value)); this.formService.presentedPageAdded .pipe(takeUntil(this.ngUnsubscribe)) .subscribe((presentedPage: number): void => this.onPresentedPageAdded(presentedPage)); @@ -88,6 +93,7 @@ export class UnitStateComponent implements OnInit, OnDestroy { private addControl = (control: FormControlElement): void => { control.formGroup.addControl(control.id, control.formControl); + this.registerUnitStateElementCode(control.id, control.formControl.value); }; private setValidators = (validators: FormControlValidators): void => { @@ -113,6 +119,12 @@ export class UnitStateComponent implements OnInit, OnDestroy { } }; + private onElementValueChanges = (value: ValueChangeElement): void => { + // eslint-disable-next-line no-console + console.log(`player: onElementValueChanges ${value.id}: old: ${value.values[0]}, new: ${value.values[1]}`); + this.setUnitStateElementCodeValue(value.id, value.values[1]); + }; + private onFormChanges(): void { // eslint-disable-next-line no-console console.log('player: onFormChanges', this.form.value); @@ -131,7 +143,7 @@ export class UnitStateComponent implements OnInit, OnDestroy { private sendVopStateChangedNotification(): void { const unitState: UnitState = { dataParts: { - pages: JSON.stringify(this.form.value.pages) + elementCodes: JSON.stringify(this.unitStateElementCodes) }, presentationProgress: this.presentationProgress, responseProgress: this.responseProgress, @@ -140,6 +152,30 @@ export class UnitStateComponent implements OnInit, OnDestroy { this.veronaPostService.sendVopStateChangedNotification({ unitState }); } + private setUnitStateElementCodeValue(id: string, value: string | number | boolean | undefined) { + const unitStateElementCode = this.unitStateElementCodes + .find((element: UnitStateElementCode): boolean => element.id === id); + if (unitStateElementCode) { + unitStateElementCode.value = value; + } + } + + private setUnitStateElementCodeStatus(id: string, status: UnitStateElementCodeStatus) { + const unitStateElementCode = this.unitStateElementCodes + .find((element: UnitStateElementCode): boolean => element.id === id); + if (unitStateElementCode) { + unitStateElementCode.status = status; + } + } + + private registerUnitStateElementCode(id: string, value: string | number | boolean | undefined) { + const unitStateElementCode = this.unitStateElementCodes + .find((element: UnitStateElementCode): boolean => element.id === id); + if (!unitStateElementCode) { + this.unitStateElementCodes.push({ id: id, value: value, status: 'NOT_REACHED' }); + } + } + ngOnDestroy(): void { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); diff --git a/projects/player/src/app/models/verona.ts b/projects/player/src/app/models/verona.ts index acfdc7090b1274c84cbad6eea3efec34226c1ed4..251c44488cb0a07d7e4e3ffc6f5b13a0744a673f 100644 --- a/projects/player/src/app/models/verona.ts +++ b/projects/player/src/app/models/verona.ts @@ -6,6 +6,8 @@ export type Progress = 'none' | 'some' | 'complete'; export type StateReportPolicy = 'none' | 'eager' | 'on-demand'; +export type UnitStateElementCodeStatus = 'NOT_REACHED' | 'DISPLAYED' | 'TOUCHED' | 'VALUE_CHANGED'; + export interface PlayerConfig { unitNumber?: number; unitTitle?: number; @@ -17,6 +19,12 @@ export interface PlayerConfig { enabledNavigationTargets?: NavigationTarget[] } +export interface UnitStateElementCode { + id: string; + status: UnitStateElementCodeStatus; + value: string | number | boolean | undefined; +} + export interface UnitState { dataParts?: Record<string, string>; presentationProgress?: Progress;