Skip to content
Snippets Groups Projects
Commit c6e771a7 authored by jojohoch's avatar jojohoch
Browse files

[player] Access Verona API Messages through Services

* Implement `VeronaSubscriptionService` to subscribe to messages from
the host of the player
* Implement `VeronaPostService` to post messages to the host of
the player
* Implement `NativeEventService` to subscribe to events like 'scroll',
'blur' and 'focus'
* Remove js code from wrapper index.html. It is no longer needed.
parent d361fc20
No related branches found
No related tags found
No related merge requests found
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'
// }
// ]
// };
}
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();
}
}
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
});
};
}
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();
}
}
......@@ -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>
......@@ -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 {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment