diff --git a/projects/player/src/app/app.component.ts b/projects/player/src/app/app.component.ts index 2e8d2e8493af14e956917c380837960550bd9604..919dfe48d88020de823a4ee0a0bcf7b79784f34a 100644 --- a/projects/player/src/app/app.component.ts +++ b/projects/player/src/app/app.component.ts @@ -1,15 +1,11 @@ -import { - Component, EventEmitter, Input, Output -} from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { Unit } from '../../../common/unit'; import { FormService } from '../../../common/form.service'; import { FormControlElement, ValueChangeElement } from '../../../common/form'; - -interface StartData { - unitDefinition: string; - unitStateData: string; -} +import { VeronaSubscriptionService } from './services/verona-subscription.service'; +import { VeronaPostService } from './services/verona-post.service'; +import { NativeEventService } from './services/native-event.service'; @Component({ selector: 'player-aspect', @@ -24,9 +20,6 @@ interface StartData { <button class="form-item" mat-flat-button color="primary" (click)="submit()">Print form.value </button> - <button class="form-item" mat-flat-button color="primary" (click)="markAsTouch()" >markAsTouch - </button> - <pre>{{unit | json}}</pre> ` }) export class AppComponent { @@ -35,19 +28,37 @@ export class AppComponent { pages: [] }; - @Input() - set startData(startData: StartData) { - this.unit = JSON.parse(startData.unitDefinition); - this.initForm(); + constructor(private formService: FormService, + private veronaSubscriptionService: VeronaSubscriptionService, + private veronaPostService: VeronaPostService, + private nativeEventService: NativeEventService) { + this.subscribe(); + veronaPostService.sendVopReadyNotification(); } - @Output() valueChanged = new EventEmitter<string>(); - - constructor(private formService: FormService) { - formService.elementValueChanged + private subscribe(): void { + this.formService.elementValueChanged .subscribe((value: ValueChangeElement): void => this.onElementValueChanges(value)); - formService.controlAdded + this.formService.controlAdded .subscribe((control: FormControlElement): void => this.addControl(control)); + + this.veronaSubscriptionService.vopStartCommand + .subscribe((data: any): void => this.onStart(data)); + this.veronaSubscriptionService.vopNavigationDeniedNotification + .subscribe((data: any): void => this.onNavigationDenied(data)); + + this.nativeEventService.scrollY + .subscribe((y: number): void => this.onScrollY(y)); + this.nativeEventService.focus + .subscribe((focused: boolean): void => this.onFocus(focused)); + } + + private onStart(data: any): void { + console.log('player: onStart', data); + this.veronaPostService.sessionId = data.sessionId; + this.unit = JSON.parse(data.unitDefinition); + // playerStartData.unitStateData = data.unitState?.dataParts?.all; + this.initForm(); } private initForm(): void { @@ -55,6 +66,11 @@ export class AppComponent { this.form.valueChanges.subscribe(v => this.onFormChanges(v)); } + private onNavigationDenied(data: any): void { + console.log('player: onNavigationDenied', data); + this.form.markAllAsTouched(); + } + private addControl(control: FormControlElement): void { this.form.addControl(control.id, control.formControl); } @@ -68,90 +84,22 @@ export class AppComponent { const allValues: string = JSON.stringify(value); // eslint-disable-next-line no-console console.log('Player: emit valueChanged', allValues); - this.valueChanged.emit(allValues); + this.veronaPostService.sendVopStateChangedNotification(allValues); + } + + private onScrollY = (y: number): void => { + console.log('player: onScrollY', y); + }; + + // TODO + private onFocus(focused: boolean): void { + console.log('player: onFocus', focused); + this.veronaPostService.sendVopWindowFocusChangedNotification(focused); } + /// ////////////////////// only for dev submit(): void { // eslint-disable-next-line no-console console.log('Player: form.value', this.form.value); } - - markAsTouch(): void { - this.form.markAllAsTouched(); - } - - // exampleUnit = { - // pages: [ - // { - // sections: [ - // { - // elements: [ - // { - // label: 'Label Dropdown', - // options: [ - // 'op1', - // 'op2' - // ], - // type: 'dropdown', - // id: 'dummyID', - // xPosition: 124, - // yPosition: 26, - // width: 180, - // height: 60, - // backgroundColor: 'grey', - // fontColor: 'blue', - // font: 'Arial', - // fontSize: 18, - // bold: true, - // italic: false, - // underline: false - // } - // ], - // width: 1200, - // height: 200, - // backgroundColor: '#FFFAF0' - // }, - // { - // elements: [ - // { - // label: 'Button Text', - // type: 'button', - // id: 'dummyID', - // xPosition: 440, - // yPosition: 77, - // width: 180, - // height: 60, - // backgroundColor: 'grey', - // fontColor: 'blue', - // font: 'Arial', - // fontSize: 18, - // bold: true, - // italic: false, - // underline: false - // } - // ], - // width: 1200, - // height: 200, - // backgroundColor: '#FFFAF0' - // } - // ], - // width: 1200, - // height: 550, - // backgroundColor: '#FFFAF0' - // }, - // { - // sections: [ - // { - // elements: [], - // width: 1200, - // height: 200, - // backgroundColor: '#FFFAF0' - // } - // ], - // width: 1200, - // height: 550, - // backgroundColor: '#FFFAF0' - // } - // ] - // }; } diff --git a/projects/player/src/app/services/native-event.service.ts b/projects/player/src/app/services/native-event.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..8640516b4dfddf09bc0cfbcf9bef106fadf0e0d9 --- /dev/null +++ b/projects/player/src/app/services/native-event.service.ts @@ -0,0 +1,34 @@ +import { Inject, Injectable } from '@angular/core'; +import { + from, fromEvent, Observable, Subject +} from 'rxjs'; +import { DOCUMENT } from '@angular/common'; +import { mergeMap } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class NativeEventService { + private _scrollY = new Subject<number>(); + private _focus = new Subject<boolean>(); + + constructor(@Inject(DOCUMENT) private document: Document) { + fromEvent(window, 'scroll') + .subscribe(() => this._scrollY.next(window.scrollY)); + from(['blur', 'focus']) + .pipe( + mergeMap(event => fromEvent(window, event)) + ) + .subscribe( + () => this._focus.next(document.hasFocus())// Do something with the event here + ); + } + + get scrollY(): Observable<number> { + return this._scrollY.asObservable(); + } + + get focus(): Observable<boolean> { + return this._focus.asObservable(); + } +} diff --git a/projects/player/src/app/services/verona-post.service.ts b/projects/player/src/app/services/verona-post.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..1644be7d5010d78a8a5aa2bf03e88df6f3da15ca --- /dev/null +++ b/projects/player/src/app/services/verona-post.service.ts @@ -0,0 +1,67 @@ +import { Inject, Injectable } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +@Injectable({ + providedIn: 'root' +}) +export class VeronaPostService { + private playerMetadata!: any; + private _sessionId!: string; + + constructor(@Inject(DOCUMENT) private document: Document) { + this.playerMetadata = document.querySelectorAll('meta')[1].attributes; + } + + set sessionId(sessionId:string) { + this._sessionId = sessionId; + } + + private isStandalone = (): boolean => window === window.parent; + + private send(message: any): void { + // prevend posts in local dev mode + if (!this.isStandalone()) { + window.parent.postMessage(message, '*'); + } + } + + sendVopStateChangedNotification(value: unknown) : void { + this.send({ + type: 'vopStateChangedNotification', + sessionId: this._sessionId, + timeStamp: Date.now(), + unitState: { + dataParts: { + all: value + }, + unitStateDataType: this.playerMetadata.getNamedItem('data-supported-unit-state-data-types').value + } + }); + } + + sendVopReadyNotification(): void { + this.send({ + type: 'vopReadyNotification', + apiVersion: this.playerMetadata.getNamedItem('data-api-version').value, + notSupportedApiFeatures: this.playerMetadata.getNamedItem('data-not-supported-api-features').value, + supportedUnitDefinitionTypes: this.playerMetadata.getNamedItem('data-supported-unit-definition-types').value, + supportedUnitStateDataTypes: this.playerMetadata.getNamedItem('data-supported-unit-state-data-types').value + }); + } + + sendVopUnitNavigationRequestedNotification = (target: string): void => { + this.send({ + type: 'vopUnitNavigationRequestedNotification', + sessionId: this._sessionId, + target: target + }); + }; + + sendVopWindowFocusChangedNotification = (focused: boolean): void => { + this.send({ + type: 'vopWindowFocusChangedNotification', + sessionId: this._sessionId, + hasFocus: focused + }); + }; +} diff --git a/projects/player/src/app/services/verona-subscription.service.ts b/projects/player/src/app/services/verona-subscription.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a192d0bd409bc1a861a9fc8f6751777fa6aeb219 --- /dev/null +++ b/projects/player/src/app/services/verona-subscription.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { fromEvent, Observable, Subject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) + +export class VeronaSubscriptionService { + private _vopStartCommand = new Subject<any>(); + private _vopNavigationDeniedNotification = new Subject<any>(); + + constructor() { + fromEvent(window, 'message') + .subscribe((e: Event): void => { + this.handleMessage((e as MessageEvent).data); + }); + } + + handleMessage(messageData: any): void { + switch (messageData.type) { + case 'vopStartCommand': + console.log('Player: _vopStartCommand ', messageData); + this._vopStartCommand.next(messageData); + break; + case 'vopNavigationDeniedNotification': + console.log('Player: _vopNavigationDeniedNotification ', messageData); + this._vopNavigationDeniedNotification.next(messageData); + break; + // TODO + case 'vopPageNavigationCommand': + case 'vopGetStateRequest': + case 'vopStopCommand': + case 'vopContinueCommand': + default: + console.warn(`player: got message of unknown type ${messageData.type}`); + } + } + + get vopStartCommand(): Observable<any> { + return this._vopStartCommand.asObservable(); + } + + get vopNavigationDeniedNotification(): Observable<any> { + return this._vopNavigationDeniedNotification.asObservable(); + } +} diff --git a/projects/player/src/html_wrapper/index.html b/projects/player/src/html_wrapper/index.html index 6b74a6035acc1dcc4978a959666efc1dfef5088a..c548a6bae666442cf6de228bd717e8ab3c605d43 100644 --- a/projects/player/src/html_wrapper/index.html +++ b/projects/player/src/html_wrapper/index.html @@ -47,97 +47,6 @@ </head> <body> <script type="text/javascript" src="player.js"></script> -<script> - let sessionId; - let playerMetadata = document.querySelectorAll("meta")[1].attributes; - let tempResponses = {}; - let playerStartData = { - unitDefinition: '', - unitStateData: '' - }; - - function elementValueChanged(event) { - window.parent.postMessage({ - type: 'vopStateChangedNotification', - sessionId: sessionId, - timeStamp: Date.now(), - unitState: { - dataParts: { - all: event.detail - }, - unitStateDataType: playerMetadata.getNamedItem('data-supported-unit-state-data-types').value, - } - }, '*'); - } - - function navigationRequested(event) { - window.parent.postMessage({ - type: 'vopUnitNavigationRequestedNotification', - sessionId: sessionId, - targetRelative: '#' + event.detail, - target: event.detail, - }, '*'); - } - - const playerComponent = document.createElement('player-aspect',{ is : 'player-aspect' }); - playerComponent.addEventListener('valueChanged', (event) => { - elementValueChanged(event); - }); - playerComponent.addEventListener('navigationRequested', (event) => { - - navigationRequested(event); - }); - document.body.appendChild(playerComponent) - - window.addEventListener('message', (event) => { - if ('data' in event && 'type' in event.data) { - switch (event.data.type) { - case 'vopStartCommand': - if (event.data.sessionId) { - sessionId = event.data.sessionId; - playerStartData.unitDefinition = event.data.unitDefinition; - playerStartData.unitStateData = event.data.unitState?.dataParts?.all; - playerComponent.startData = playerStartData; - } else { - console.error('player: (vopStartCommand) no sessionId is given'); - } - break; - case 'vopPageNavigationCommand': - case 'vopGetStateRequest': - case 'vopStopCommand': - case 'vopContinueCommand': - console.warn(`player: message of type ${event.data.type} not processed yet`); - break; - case 'vopNavigationDeniedNotification': - console.info(['player: got vopNavigationDeniedNotification']); - // playerComponent.tryLeaveNotify(); - break; - default: - console.warn(`player: got message of unknown type ${event.data.type}`); - } - } - }); - window.addEventListener('blur', () => { - window.parent.postMessage({ - type: 'vopWindowFocusChangedNotification', - sessionId: sessionId, - hasFocus: document.hasFocus() - }, '*'); - }); - window.addEventListener('focus', () => { - window.parent.postMessage({ - type: 'vopWindowFocusChangedNotification', - sessionId: sessionId, - hasFocus: document.hasFocus() - }, '*'); - }); - window.parent.postMessage({ - type: 'vopReadyNotification', - apiVersion: playerMetadata.getNamedItem('data-api-version').value, - notSupportedApiFeatures: playerMetadata.getNamedItem('data-not-supported-api-features').value, - supportedUnitDefinitionTypes: playerMetadata.getNamedItem('data-supported-unit-definition-types').value, - supportedUnitStateDataTypes: playerMetadata.getNamedItem('data-supported-unit-state-data-types').value, - }, '*'); -</script> +<player-aspect></player-aspect> </body> </html> diff --git a/projects/player/src/index.html b/projects/player/src/index.html index 8239c6239c05d61458c18ebca572c8367ca20e82..43c033de1cc778a7900634da5dac3c04fc00f31a 100644 --- a/projects/player/src/index.html +++ b/projects/player/src/index.html @@ -3,6 +3,15 @@ <head> <meta charset="utf-8"> <title>Player</title> + <meta name="application-name" content="iqb-player-aspect" + data-version="3.3.3" + data-repository-url="https://github.com/iqb-berlin/verona-modules-apect" + data-api-version="2.1.0" + data-not-supported-api-features="" + data-supported-unit-definition-types="iqb-scripted@1.0" + data-supported-unit-state-data-types="iqb-key-value@1.0.0" + data-supported-browsers='{"Firefox": 69, "Chrome": 72, "Edge": 79}' + /> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> @@ -15,22 +24,32 @@ <input type='button' value='Load' onclick='loadUnitDefinition();'> </fieldset> </form> +<button onclick='denyNavigation();'>Deny Navigation</button> <player-aspect></player-aspect> </body> <script type="text/javascript"> - - const playerComponent = document.getElementsByTagName('player-aspect').item(0); - - setStartData = (e) => { - playerComponent.startData = { - unitDefinition: e.target.result, - unitStateData: '' - }; + denyNavigation = () => { + window.postMessage({ + type: 'vopNavigationDeniedNotification', + sessionId: 'dev', + reason: [] + }, '*'); } loadUnitDefinition = () => { - let input = document.getElementById('fileInput'); + const setStartData = (e) => { + window.postMessage({ + type: 'vopStartCommand', + sessionId: 'dev', + unitDefinition: e.target.result, + unitState: { + dataParts: {} + }, + playerConfig: {} + }, '*'); + } + const input = document.getElementById('fileInput'); if (!input['files'][0]) { alert("Please choose a file before clicking 'Load'"); } else {