Merge pull request #790 from thecodingmachine/iframe_api
Adding an API for inter-iframe communication
This commit is contained in:
commit
02fb42b68a
62 changed files with 3122 additions and 37 deletions
|
@ -8,6 +8,11 @@ FROM thecodingmachine/nodejs:14-apache
|
|||
|
||||
COPY --chown=docker:docker front .
|
||||
COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated
|
||||
|
||||
# Removing the iframe.html file from the final image as this adds a XSS attack.
|
||||
# iframe.html is only in dev mode to circumvent a limitation
|
||||
RUN rm dist/iframe.html
|
||||
|
||||
RUN yarn install
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
|
3
front/dist/.gitignore
vendored
3
front/dist/.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
index.html
|
||||
index.tmpl.html.tmp
|
||||
style.*.css
|
||||
/js/
|
||||
style.*.css
|
||||
|
|
17
front/dist/iframe.html
vendored
Normal file
17
front/dist/iframe.html
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="/iframe_api.js" ></script>
|
||||
<script>
|
||||
// Note: this is a huge XSS flow as we allow anyone to load a Javascript file in our domain.
|
||||
// This file must ABSOLUTELY be removed from the Docker images/deployments and is only here
|
||||
// for development purpose (because dynamically generated iframes are not working with
|
||||
// webpack hot reload due to an issue with rights)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const scriptUrl = urlParams.get('script');
|
||||
const script = document.createElement('script');
|
||||
script.src = scriptUrl;
|
||||
document.head.append(script);
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
3
front/dist/index.tmpl.html
vendored
3
front/dist/index.tmpl.html
vendored
|
@ -29,6 +29,9 @@
|
|||
|
||||
|
||||
<base href="/">
|
||||
<link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet">
|
||||
<link href="https://unpkg.com/nes.css@2.3.0/css/nes.min.css" rel="stylesheet" />
|
||||
|
||||
<title>WorkAdventure</title>
|
||||
</head>
|
||||
<body id="body" style="margin: 0; background-color: #000">
|
||||
|
|
25
front/dist/resources/style/style.css
vendored
25
front/dist/resources/style/style.css
vendored
|
@ -1123,6 +1123,31 @@ div.action p.action-body{
|
|||
margin-left: calc(50% - 75px);
|
||||
border-radius: 15px;
|
||||
}
|
||||
.popUpElement{
|
||||
font-family: 'Press Start 2P';
|
||||
text-align: left;
|
||||
color: white;
|
||||
}
|
||||
.popUpElement div {
|
||||
font-family: 'Press Start 2P';
|
||||
font-size: 10px;
|
||||
background-color: #727678;
|
||||
}
|
||||
|
||||
.popUpElement button {
|
||||
position: relative;
|
||||
font-size: 10px;
|
||||
border-image-repeat: revert;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.popUpElement .buttonContainer {
|
||||
float: right;
|
||||
background-color: inherit;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@keyframes mymove {
|
||||
0% {bottom: 40px;}
|
||||
50% {bottom: 30px;}
|
||||
|
|
|
@ -336,7 +336,7 @@ export class ConsoleGlobalMessageManager {
|
|||
}
|
||||
|
||||
active(){
|
||||
this.userInputManager.clearAllKeys();
|
||||
this.userInputManager.disableControls();
|
||||
this.divMainConsole.style.top = '0';
|
||||
this.activeConsole = true;
|
||||
}
|
||||
|
|
11
front/src/Api/Events/ButtonClickedEvent.ts
Normal file
11
front/src/Api/Events/ButtonClickedEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isButtonClickedEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
popupId: tg.isNumber,
|
||||
buttonId: tg.isNumber,
|
||||
}).get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
|
||||
*/
|
||||
export type ButtonClickedEvent = tg.GuardedType<typeof isButtonClickedEvent>;
|
11
front/src/Api/Events/ChatEvent.ts
Normal file
11
front/src/Api/Events/ChatEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isChatEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
message: tg.isString,
|
||||
author: tg.isString,
|
||||
}).get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;
|
11
front/src/Api/Events/ClosePopupEvent.ts
Normal file
11
front/src/Api/Events/ClosePopupEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isClosePopupEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
popupId: tg.isNumber,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type ClosePopupEvent = tg.GuardedType<typeof isClosePopupEvent>;
|
10
front/src/Api/Events/EnterLeaveEvent.ts
Normal file
10
front/src/Api/Events/EnterLeaveEvent.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isEnterLeaveEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
name: tg.isString,
|
||||
}).get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
|
||||
*/
|
||||
export type EnterLeaveEvent = tg.GuardedType<typeof isEnterLeaveEvent>;
|
13
front/src/Api/Events/GoToPageEvent.ts
Normal file
13
front/src/Api/Events/GoToPageEvent.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isGoToPageEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type GoToPageEvent = tg.GuardedType<typeof isGoToPageEvent>;
|
7
front/src/Api/Events/IframeEvent.ts
Normal file
7
front/src/Api/Events/IframeEvent.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export interface IframeEvent {
|
||||
type: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIframeEventWrapper = (event: any): event is IframeEvent => typeof event.type === 'string';
|
13
front/src/Api/Events/OpenCoWebSiteEvent.ts
Normal file
13
front/src/Api/Events/OpenCoWebSiteEvent.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isOpenCoWebsite =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type OpenCoWebSiteEvent = tg.GuardedType<typeof isOpenCoWebsite>;
|
20
front/src/Api/Events/OpenPopupEvent.ts
Normal file
20
front/src/Api/Events/OpenPopupEvent.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
const isButtonDescriptor =
|
||||
new tg.IsInterface().withProperties({
|
||||
label: tg.isString,
|
||||
className: tg.isOptional(tg.isString)
|
||||
}).get();
|
||||
|
||||
export const isOpenPopupEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
popupId: tg.isNumber,
|
||||
targetObject: tg.isString,
|
||||
message: tg.isString,
|
||||
buttons: tg.isArray(isButtonDescriptor)
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type OpenPopupEvent = tg.GuardedType<typeof isOpenPopupEvent>;
|
13
front/src/Api/Events/OpenTabEvent.ts
Normal file
13
front/src/Api/Events/OpenTabEvent.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isOpenTabEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type OpenTabEvent = tg.GuardedType<typeof isOpenTabEvent>;
|
10
front/src/Api/Events/UserInputChatEvent.ts
Normal file
10
front/src/Api/Events/UserInputChatEvent.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isUserInputChatEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
message: tg.isString,
|
||||
}).get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when a user types a message in the chat.
|
||||
*/
|
||||
export type UserInputChatEvent = tg.GuardedType<typeof isUserInputChatEvent>;
|
238
front/src/Api/IframeListener.ts
Normal file
238
front/src/Api/IframeListener.ts
Normal file
|
@ -0,0 +1,238 @@
|
|||
import {Subject} from "rxjs";
|
||||
import {ChatEvent, isChatEvent} from "./Events/ChatEvent";
|
||||
import {IframeEvent, isIframeEventWrapper} from "./Events/IframeEvent";
|
||||
import {UserInputChatEvent} from "./Events/UserInputChatEvent";
|
||||
import * as crypto from "crypto";
|
||||
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
||||
import {EnterLeaveEvent} from "./Events/EnterLeaveEvent";
|
||||
import {isOpenPopupEvent, OpenPopupEvent} from "./Events/OpenPopupEvent";
|
||||
import {isOpenTabEvent, OpenTabEvent} from "./Events/OpenTabEvent";
|
||||
import {ButtonClickedEvent} from "./Events/ButtonClickedEvent";
|
||||
import {ClosePopupEvent, isClosePopupEvent} from "./Events/ClosePopupEvent";
|
||||
import {scriptUtils} from "./ScriptUtils";
|
||||
import {GoToPageEvent, isGoToPageEvent} from "./Events/GoToPageEvent";
|
||||
import {isOpenCoWebsite, OpenCoWebSiteEvent} from "./Events/OpenCoWebSiteEvent";
|
||||
|
||||
|
||||
/**
|
||||
* Listens to messages from iframes and turn those messages into easy to use observables.
|
||||
* Also allows to send messages to those iframes.
|
||||
*/
|
||||
class IframeListener {
|
||||
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
||||
public readonly chatStream = this._chatStream.asObservable();
|
||||
|
||||
private readonly _openPopupStream: Subject<OpenPopupEvent> = new Subject();
|
||||
public readonly openPopupStream = this._openPopupStream.asObservable();
|
||||
|
||||
private readonly _openTabStream: Subject<OpenTabEvent> = new Subject();
|
||||
public readonly openTabStream = this._openTabStream.asObservable();
|
||||
|
||||
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
|
||||
public readonly goToPageStream = this._goToPageStream.asObservable();
|
||||
|
||||
private readonly _openCoWebSiteStream: Subject<OpenCoWebSiteEvent> = new Subject();
|
||||
public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable();
|
||||
|
||||
private readonly _closeCoWebSiteStream: Subject<void> = new Subject();
|
||||
public readonly closeCoWebSiteStream = this._closeCoWebSiteStream.asObservable();
|
||||
|
||||
private readonly _disablePlayerControlStream: Subject<void> = new Subject();
|
||||
public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable();
|
||||
|
||||
private readonly _enablePlayerControlStream: Subject<void> = new Subject();
|
||||
public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable();
|
||||
|
||||
private readonly _closePopupStream: Subject<ClosePopupEvent> = new Subject();
|
||||
public readonly closePopupStream = this._closePopupStream.asObservable();
|
||||
|
||||
private readonly _displayBubbleStream: Subject<void> = new Subject();
|
||||
public readonly displayBubbleStream = this._displayBubbleStream.asObservable();
|
||||
|
||||
private readonly _removeBubbleStream: Subject<void> = new Subject();
|
||||
public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
|
||||
|
||||
private readonly iframes = new Set<HTMLIFrameElement>();
|
||||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||
|
||||
init() {
|
||||
window.addEventListener("message", (message) => {
|
||||
// Do we trust the sender of this message?
|
||||
// Let's only accept messages from the iframe that are allowed.
|
||||
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
|
||||
let found = false;
|
||||
for (const iframe of this.iframes) {
|
||||
if (iframe.contentWindow === message.source) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = message.data;
|
||||
if (isIframeEventWrapper(payload)) {
|
||||
if (payload.type === 'chat' && isChatEvent(payload.data)) {
|
||||
this._chatStream.next(payload.data);
|
||||
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
|
||||
this._openPopupStream.next(payload.data);
|
||||
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
|
||||
this._closePopupStream.next(payload.data);
|
||||
}
|
||||
else if(payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
|
||||
scriptUtils.openTab(payload.data.url);
|
||||
}
|
||||
else if(payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
|
||||
scriptUtils.goToPage(payload.data.url);
|
||||
}
|
||||
else if(payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
|
||||
scriptUtils.openCoWebsite(payload.data.url);
|
||||
}
|
||||
else if(payload.type === 'closeCoWebSite') {
|
||||
scriptUtils.closeCoWebSite();
|
||||
}
|
||||
else if (payload.type === 'disablePlayerControl'){
|
||||
this._disablePlayerControlStream.next();
|
||||
}
|
||||
else if (payload.type === 'restorePlayerControl'){
|
||||
this._enablePlayerControlStream.next();
|
||||
}
|
||||
else if (payload.type === 'displayBubble'){
|
||||
this._displayBubbleStream.next();
|
||||
}
|
||||
else if (payload.type === 'removeBubble'){
|
||||
this._removeBubbleStream.next();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}, false);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the passed iFrame to send/receive messages via the API.
|
||||
*/
|
||||
registerIframe(iframe: HTMLIFrameElement): void {
|
||||
this.iframes.add(iframe);
|
||||
}
|
||||
|
||||
unregisterIframe(iframe: HTMLIFrameElement): void {
|
||||
this.iframes.delete(iframe);
|
||||
}
|
||||
|
||||
registerScript(scriptUrl: string): void {
|
||||
console.log('Loading map related script at ', scriptUrl)
|
||||
|
||||
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
|
||||
// Using external iframe mode (
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = this.getIFrameId(scriptUrl);
|
||||
iframe.style.display = 'none';
|
||||
iframe.src = '/iframe.html?script='+encodeURIComponent(scriptUrl);
|
||||
|
||||
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
||||
iframe.sandbox.add('allow-scripts');
|
||||
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
||||
|
||||
document.body.prepend(iframe);
|
||||
|
||||
this.scripts.set(scriptUrl, iframe);
|
||||
this.registerIframe(iframe);
|
||||
} else {
|
||||
// production code
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = this.getIFrameId(scriptUrl);
|
||||
iframe.style.display = 'none';
|
||||
|
||||
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
||||
iframe.sandbox.add('allow-scripts');
|
||||
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
||||
|
||||
const html = '<!doctype html>\n' +
|
||||
'\n' +
|
||||
'<html lang="en">\n' +
|
||||
'<head>\n' +
|
||||
'<script src="'+window.location.protocol+'//'+window.location.host+'/iframe_api.js" ></script>\n' +
|
||||
'<script src="'+scriptUrl+'" ></script>\n' +
|
||||
'</head>\n' +
|
||||
'</html>\n';
|
||||
|
||||
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
|
||||
iframe.srcdoc = html;
|
||||
|
||||
document.body.prepend(iframe);
|
||||
|
||||
this.scripts.set(scriptUrl, iframe);
|
||||
this.registerIframe(iframe);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private getIFrameId(scriptUrl: string): string {
|
||||
return 'script'+crypto.createHash('md5').update(scriptUrl).digest("hex");
|
||||
}
|
||||
|
||||
unregisterScript(scriptUrl: string): void {
|
||||
const iFrameId = this.getIFrameId(scriptUrl);
|
||||
const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId);
|
||||
if (!iframe) {
|
||||
throw new Error('Unknown iframe for script "'+scriptUrl+'"');
|
||||
}
|
||||
this.unregisterIframe(iframe);
|
||||
iframe.remove();
|
||||
|
||||
this.scripts.delete(scriptUrl);
|
||||
}
|
||||
|
||||
sendUserInputChat(message: string) {
|
||||
this.postMessage({
|
||||
'type': 'userInputChat',
|
||||
'data': {
|
||||
'message': message,
|
||||
} as UserInputChatEvent
|
||||
});
|
||||
}
|
||||
|
||||
sendEnterEvent(name: string) {
|
||||
this.postMessage({
|
||||
'type': 'enterEvent',
|
||||
'data': {
|
||||
"name": name
|
||||
} as EnterLeaveEvent
|
||||
});
|
||||
}
|
||||
|
||||
sendLeaveEvent(name: string) {
|
||||
this.postMessage({
|
||||
'type': 'leaveEvent',
|
||||
'data': {
|
||||
"name": name
|
||||
} as EnterLeaveEvent
|
||||
});
|
||||
}
|
||||
|
||||
sendButtonClickedEvent(popupId: number, buttonId: number): void {
|
||||
this.postMessage({
|
||||
'type': 'buttonClickedEvent',
|
||||
'data': {
|
||||
popupId,
|
||||
buttonId
|
||||
} as ButtonClickedEvent
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message... to all allowed iframes.
|
||||
*/
|
||||
private postMessage(message: IframeEvent) {
|
||||
for (const iframe of this.iframes) {
|
||||
iframe.contentWindow?.postMessage(message, '*');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const iframeListener = new IframeListener();
|
23
front/src/Api/ScriptUtils.ts
Normal file
23
front/src/Api/ScriptUtils.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import {coWebsiteManager} from "../WebRtc/CoWebsiteManager";
|
||||
|
||||
class ScriptUtils {
|
||||
|
||||
public openTab(url : string){
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
public goToPage(url : string){
|
||||
window.location.href = url;
|
||||
|
||||
}
|
||||
|
||||
public openCoWebsite(url : string){
|
||||
coWebsiteManager.loadCoWebsite(url,url);
|
||||
}
|
||||
|
||||
public closeCoWebSite(){
|
||||
coWebsiteManager.closeCoWebsite();
|
||||
}
|
||||
}
|
||||
|
||||
export const scriptUtils = new ScriptUtils();
|
|
@ -59,11 +59,14 @@ import {TextureError} from "../../Exception/TextureError";
|
|||
import {addLoader} from "../Components/Loader";
|
||||
import {ErrorSceneName} from "../Reconnecting/ErrorScene";
|
||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
||||
import {iframeListener} from "../../Api/IframeListener";
|
||||
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
|
||||
import Texture = Phaser.Textures.Texture;
|
||||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
import CanvasTexture = Phaser.Textures.CanvasTexture;
|
||||
import GameObject = Phaser.GameObjects.GameObject;
|
||||
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
||||
import DOMElement = Phaser.GameObjects.DOMElement;
|
||||
import {Subscription} from "rxjs";
|
||||
import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream";
|
||||
|
||||
|
@ -157,6 +160,7 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
private playerName!: string;
|
||||
private characterLayers!: string[];
|
||||
private messageSubscription: Subscription|null = null;
|
||||
private popUpElements : Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>();
|
||||
|
||||
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
|
||||
super({
|
||||
|
@ -263,7 +267,8 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error('Unsupported object type: "'+ itemType +'"');
|
||||
continue;
|
||||
//throw new Error('Unsupported object type: "'+ itemType +'"');
|
||||
}
|
||||
|
||||
itemFactory.preload(this.load);
|
||||
|
@ -289,6 +294,12 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Now, let's load the script, if any
|
||||
const scripts = this.getScriptUrls(this.mapFile);
|
||||
for (const script of scripts) {
|
||||
iframeListener.registerScript(script);
|
||||
}
|
||||
}
|
||||
|
||||
//hook initialisation
|
||||
|
@ -306,7 +317,7 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
gameManager.gameSceneIsCreated(this);
|
||||
urlManager.pushRoomIdToUrl(this.room);
|
||||
this.startLayerName = urlManager.getStartLayerNameFromUrl();
|
||||
|
||||
|
||||
this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => this.showWorldFullError())
|
||||
|
||||
const playerName = gameManager.getPlayerName();
|
||||
|
@ -410,6 +421,7 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
// From now, this game scene will be notified of reposition events
|
||||
layoutManager.setListener(this);
|
||||
this.triggerOnMapLayerPropertyChange();
|
||||
this.listenToIframeEvents();
|
||||
|
||||
const camera = this.cameras.main;
|
||||
|
||||
|
@ -577,12 +589,12 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
const contextRed = this.circleRedTexture.context;
|
||||
contextRed.beginPath();
|
||||
contextRed.arc(48, 48, 48, 0, 2 * Math.PI, false);
|
||||
// context.lineWidth = 5;
|
||||
//context.lineWidth = 5;
|
||||
contextRed.strokeStyle = '#ff0000';
|
||||
contextRed.stroke();
|
||||
this.circleRedTexture.refresh();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private safeParseJSONstring(jsonString: string|undefined, propertyName: string) {
|
||||
try {
|
||||
|
@ -606,7 +618,7 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
coWebsiteManager.closeCoWebsite();
|
||||
}else{
|
||||
const openWebsiteFunction = () => {
|
||||
coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsitePolicy') as string | undefined);
|
||||
coWebsiteManager.loadCoWebsite(newValue as string, this.MapUrlFile, allProps.get('openWebsiteAllowApi') as boolean | undefined, allProps.get('openWebsitePolicy') as string | undefined);
|
||||
layoutManager.removeActionButton('openWebsite', this.userInputManager);
|
||||
};
|
||||
|
||||
|
@ -672,6 +684,103 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
this.gameMap.onPropertyChange('playAudioLoop', (newValue, oldValue) => {
|
||||
newValue === undefined ? audioManager.unloadAudio() : audioManager.playAudio(newValue, this.getMapDirUrl(), undefined, true);
|
||||
});
|
||||
|
||||
this.gameMap.onPropertyChange('zone', (newValue, oldValue) => {
|
||||
if (newValue === undefined || newValue === false || newValue === '') {
|
||||
iframeListener.sendLeaveEvent(oldValue as string);
|
||||
|
||||
} else {
|
||||
iframeListener.sendEnterEvent(newValue as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private listenToIframeEvents(): void {
|
||||
iframeListener.openPopupStream.subscribe((openPopupEvent) => {
|
||||
|
||||
let objectLayerSquare : ITiledMapObject;
|
||||
const targetObjectData = this.getObjectLayerData(openPopupEvent.targetObject);
|
||||
if (targetObjectData !== undefined){
|
||||
objectLayerSquare = targetObjectData;
|
||||
} else {
|
||||
console.error("Error while opening a popup. Cannot find an object on the map with name '" + openPopupEvent.targetObject + "'. The first parameter of WA.openPopup() must be the name of a rectangle object in your map.");
|
||||
return;
|
||||
}
|
||||
const escapedMessage = HtmlUtils.escapeHtml(openPopupEvent.message);
|
||||
let html = `<div id="container"><div class="nes-container with-title is-centered">
|
||||
${escapedMessage}
|
||||
</div> </div>`;
|
||||
const buttonContainer = `<div class="buttonContainer"</div>`;
|
||||
html += buttonContainer;
|
||||
let id = 0;
|
||||
for (const button of openPopupEvent.buttons) {
|
||||
html += `<button type="button" class="nes-btn is-${HtmlUtils.escapeHtml(button.className ?? '')}" id="popup-${openPopupEvent.popupId}-${id}">${HtmlUtils.escapeHtml(button.label)}</button>`;
|
||||
id++;
|
||||
}
|
||||
const domElement = this.add.dom(objectLayerSquare.x + objectLayerSquare.width/2 ,
|
||||
objectLayerSquare.y + objectLayerSquare.height/2).createFromHTML(html);
|
||||
|
||||
const container : HTMLDivElement = domElement.getChildByID("container") as HTMLDivElement;
|
||||
container.style.width = objectLayerSquare.width + "px";
|
||||
domElement.scale = 0;
|
||||
domElement.setClassName('popUpElement');
|
||||
|
||||
|
||||
|
||||
id = 0;
|
||||
for (const button of openPopupEvent.buttons) {
|
||||
const button = HtmlUtils.getElementByIdOrFail<HTMLButtonElement>(`popup-${openPopupEvent.popupId}-${id}`);
|
||||
const btnId = id;
|
||||
button.onclick = () => {
|
||||
iframeListener.sendButtonClickedEvent(openPopupEvent.popupId, btnId);
|
||||
}
|
||||
id++;
|
||||
}
|
||||
|
||||
this.tweens.add({
|
||||
targets : domElement ,
|
||||
scale : 1,
|
||||
ease : "EaseOut",
|
||||
duration : 400,
|
||||
});
|
||||
|
||||
this.popUpElements.set(openPopupEvent.popupId, domElement);
|
||||
});
|
||||
|
||||
iframeListener.closePopupStream.subscribe((closePopupEvent) => {
|
||||
const popUpElement = this.popUpElements.get(closePopupEvent.popupId);
|
||||
if (popUpElement === undefined) {
|
||||
console.error('Could not close popup with ID ', closePopupEvent.popupId,'. Maybe it has already been closed?');
|
||||
}
|
||||
|
||||
this.tweens.add({
|
||||
targets : popUpElement ,
|
||||
scale : 0,
|
||||
ease : "EaseOut",
|
||||
duration : 400,
|
||||
onComplete : () => {
|
||||
popUpElement?.destroy();
|
||||
this.popUpElements.delete(closePopupEvent.popupId);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
iframeListener.disablePlayerControlStream.subscribe(()=>{
|
||||
this.userInputManager.disableControls();
|
||||
})
|
||||
iframeListener.enablePlayerControlStream.subscribe(()=>{
|
||||
this.userInputManager.restoreControls();
|
||||
})
|
||||
let scriptedBubbleSprite : Sprite;
|
||||
iframeListener.displayBubbleStream.subscribe(()=>{
|
||||
scriptedBubbleSprite = new Sprite(this,this.CurrentPlayer.x + 25,this.CurrentPlayer.y,'circleSprite-white');
|
||||
scriptedBubbleSprite.setDisplayOrigin(48, 48);
|
||||
this.add.existing(scriptedBubbleSprite);
|
||||
})
|
||||
iframeListener.removeBubbleStream.subscribe(()=>{
|
||||
scriptedBubbleSprite.destroy();
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private getMapDirUrl(): string {
|
||||
|
@ -702,6 +811,12 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
public cleanupClosingScene(): void {
|
||||
// stop playing audio, close any open website, stop any open Jitsi
|
||||
coWebsiteManager.closeCoWebsite();
|
||||
// Stop the script, if any
|
||||
const scripts = this.getScriptUrls(this.mapFile);
|
||||
for (const script of scripts) {
|
||||
iframeListener.unregisterScript(script);
|
||||
}
|
||||
|
||||
this.stopJitsi();
|
||||
audioManager.unloadAudio();
|
||||
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
|
||||
|
@ -785,8 +900,12 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
return this.getProperty(layer, "startLayer") == true;
|
||||
}
|
||||
|
||||
private getProperty(layer: ITiledMapLayer, name: string): string|boolean|number|undefined {
|
||||
const properties = layer.properties;
|
||||
private getScriptUrls(map: ITiledMap): string[] {
|
||||
return (this.getProperties(map, "script") as string[]).map((script) => (new URL(script, this.MapUrlFile)).toString());
|
||||
}
|
||||
|
||||
private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined {
|
||||
const properties: ITiledMapLayerProperty[] = layer.properties;
|
||||
if (!properties) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -797,6 +916,14 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
return obj.value;
|
||||
}
|
||||
|
||||
private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] {
|
||||
const properties: ITiledMapLayerProperty[] = layer.properties;
|
||||
if (!properties) {
|
||||
return [];
|
||||
}
|
||||
return properties.filter((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()).map((property) => property.value);
|
||||
}
|
||||
|
||||
//todo: push that into the gameManager
|
||||
private async loadNextGame(exitSceneIdentifier: string){
|
||||
const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
|
||||
|
@ -1176,7 +1303,19 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
bottom: camera.scrollY + camera.height,
|
||||
});
|
||||
}
|
||||
private getObjectLayerData(objectName : string) : ITiledMapObject| undefined{
|
||||
for (const layer of this.mapFile.layers) {
|
||||
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
|
||||
for (const object of layer.objects) {
|
||||
if (object.name === objectName) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
}
|
||||
private reposition(): void {
|
||||
this.presentationModeSprite.setY(this.game.renderer.height - 2);
|
||||
this.chatModeSprite.setY(this.game.renderer.height - 2);
|
||||
|
@ -1233,7 +1372,7 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
//todo: put this into an 'orchestrator' scene (EntryScene?)
|
||||
private bannedUser(){
|
||||
this.cleanupClosingScene();
|
||||
this.userInputManager.clearAllKeys();
|
||||
this.userInputManager.disableControls();
|
||||
this.scene.start(ErrorSceneName, {
|
||||
title: 'Banned',
|
||||
subTitle: 'You were banned from WorkAdventure',
|
||||
|
@ -1245,7 +1384,7 @@ export class GameScene extends ResizableScene implements CenterListener {
|
|||
private showWorldFullError(): void {
|
||||
this.cleanupClosingScene();
|
||||
this.scene.stop(ReconnectingSceneName);
|
||||
this.userInputManager.clearAllKeys();
|
||||
this.userInputManager.disableControls();
|
||||
this.scene.start(ErrorSceneName, {
|
||||
title: 'Connection rejected',
|
||||
subTitle: 'The world you are trying to join is full. Try again later.',
|
||||
|
|
|
@ -14,7 +14,7 @@ export interface ITiledMap {
|
|||
* Map orientation (orthogonal)
|
||||
*/
|
||||
orientation: string;
|
||||
properties: {[key: string]: string};
|
||||
properties: ITiledMapLayerProperty[];
|
||||
|
||||
/**
|
||||
* Render order (right-down)
|
||||
|
|
|
@ -61,7 +61,7 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
|
|||
|
||||
this.opened = true;
|
||||
|
||||
gameManager.getCurrentGameScene(this.scene).userInputManager.clearAllKeys();
|
||||
gameManager.getCurrentGameScene(this.scene).userInputManager.disableControls();
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: this,
|
||||
|
|
|
@ -31,10 +31,11 @@ export class ActiveEventList {
|
|||
export class UserInputManager {
|
||||
private KeysCode!: UserInputManagerDatum[];
|
||||
private Scene: GameScene;
|
||||
|
||||
private isInputDisabled : boolean;
|
||||
constructor(Scene : GameScene) {
|
||||
this.Scene = Scene;
|
||||
this.initKeyBoardEvent();
|
||||
this.isInputDisabled = false;
|
||||
}
|
||||
|
||||
initKeyBoardEvent(){
|
||||
|
@ -63,16 +64,25 @@ export class UserInputManager {
|
|||
this.Scene.input.keyboard.removeAllListeners();
|
||||
}
|
||||
|
||||
clearAllKeys(){
|
||||
disableControls(){
|
||||
this.Scene.input.keyboard.removeAllKeys();
|
||||
this.isInputDisabled = true;
|
||||
}
|
||||
|
||||
restoreControls(){
|
||||
this.initKeyBoardEvent();
|
||||
this.isInputDisabled = false;
|
||||
}
|
||||
getEventListForGameTick(): ActiveEventList {
|
||||
const eventsMap = new ActiveEventList();
|
||||
if (this.isInputDisabled) {
|
||||
return eventsMap;
|
||||
}
|
||||
this.KeysCode.forEach(d => {
|
||||
if (d. keyInstance.isDown) {
|
||||
eventsMap.set(d.event, true);
|
||||
}
|
||||
|
||||
});
|
||||
return eventsMap;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {HtmlUtils} from "./HtmlUtils";
|
||||
import {Subject} from "rxjs";
|
||||
import {iframeListener} from "../Api/IframeListener";
|
||||
|
||||
enum iframeStates {
|
||||
closed = 1,
|
||||
|
@ -17,8 +18,8 @@ const cowebsiteCloseFullScreenImageId = 'cowebsite-fullscreen-close';
|
|||
const animationTime = 500; //time used by the css transitions, in ms.
|
||||
|
||||
class CoWebsiteManager {
|
||||
|
||||
private opened: iframeStates = iframeStates.closed;
|
||||
|
||||
private opened: iframeStates = iframeStates.closed;
|
||||
|
||||
private _onResize: Subject<void> = new Subject();
|
||||
public onResize = this._onResize.asObservable();
|
||||
|
@ -27,11 +28,11 @@ class CoWebsiteManager {
|
|||
* So we use this promise to queue up every cowebsite state transition
|
||||
*/
|
||||
private currentOperationPromise: Promise<void> = Promise.resolve();
|
||||
private cowebsiteDiv: HTMLDivElement;
|
||||
private cowebsiteDiv: HTMLDivElement;
|
||||
private resizing: boolean = false;
|
||||
private cowebsiteMainDom: HTMLDivElement;
|
||||
private cowebsiteAsideDom: HTMLDivElement;
|
||||
|
||||
|
||||
get width(): number {
|
||||
return this.cowebsiteDiv.clientWidth;
|
||||
}
|
||||
|
@ -47,15 +48,15 @@ class CoWebsiteManager {
|
|||
set height(height: number) {
|
||||
this.cowebsiteDiv.style.height = height+'px';
|
||||
}
|
||||
|
||||
|
||||
get verticalMode(): boolean {
|
||||
return window.innerWidth < window.innerHeight;
|
||||
}
|
||||
|
||||
|
||||
get isFullScreen(): boolean {
|
||||
return this.verticalMode ? this.height === window.innerHeight : this.width === window.innerWidth;
|
||||
}
|
||||
|
||||
|
||||
constructor() {
|
||||
this.cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId);
|
||||
this.cowebsiteMainDom = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteMainDomId);
|
||||
|
@ -76,7 +77,7 @@ class CoWebsiteManager {
|
|||
this.verticalMode ? this.height -= event.movementY / this.getDevicePixelRatio() : this.width -= event.movementX / this.getDevicePixelRatio();
|
||||
this.fire();
|
||||
}
|
||||
|
||||
|
||||
this.cowebsiteAsideDom.addEventListener('mousedown', (event) => {
|
||||
this.resizing = true;
|
||||
this.getIframeDom().style.display = 'none';
|
||||
|
@ -91,7 +92,7 @@ class CoWebsiteManager {
|
|||
this.resizing = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private getDevicePixelRatio(): number {
|
||||
//on chrome engines, movementX and movementY return global screens coordinates while other browser return pixels
|
||||
//so on chrome-based browser we need to adjust using 'devicePixelRatio'
|
||||
|
@ -126,7 +127,7 @@ class CoWebsiteManager {
|
|||
return iframe;
|
||||
}
|
||||
|
||||
public loadCoWebsite(url: string, base: string, allowPolicy?: string): void {
|
||||
public loadCoWebsite(url: string, base: string, allowApi?: boolean, allowPolicy?: string): void {
|
||||
this.load();
|
||||
this.cowebsiteMainDom.innerHTML = ``;
|
||||
|
||||
|
@ -134,11 +135,14 @@ class CoWebsiteManager {
|
|||
iframe.id = 'cowebsite-iframe';
|
||||
iframe.src = (new URL(url, base)).toString();
|
||||
if (allowPolicy) {
|
||||
iframe.allow = allowPolicy;
|
||||
iframe.allow = allowPolicy;
|
||||
}
|
||||
const onloadPromise = new Promise((resolve) => {
|
||||
iframe.onload = () => resolve();
|
||||
});
|
||||
if (allowApi) {
|
||||
iframeListener.registerIframe(iframe);
|
||||
}
|
||||
this.cowebsiteMainDom.appendChild(iframe);
|
||||
const onTimeoutPromise = new Promise((resolve) => {
|
||||
setTimeout(() => resolve(), 2000);
|
||||
|
@ -170,6 +174,10 @@ class CoWebsiteManager {
|
|||
if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example
|
||||
this.close();
|
||||
this.fire();
|
||||
const iframe = this.cowebsiteDiv.querySelector('iframe');
|
||||
if (iframe) {
|
||||
iframeListener.unregisterIframe(iframe);
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.cowebsiteMainDom.innerHTML = ``;
|
||||
resolve();
|
||||
|
@ -197,11 +205,11 @@ class CoWebsiteManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fire(): void {
|
||||
this._onResize.next();
|
||||
}
|
||||
|
||||
|
||||
private fullscreen(): void {
|
||||
if (this.isFullScreen) {
|
||||
this.resetStyle();
|
||||
|
|
|
@ -3,6 +3,7 @@ import {mediaManager, ReportCallback, ShowReportCallBack} from "./MediaManager";
|
|||
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
|
||||
import {connectionManager} from "../Connexion/ConnectionManager";
|
||||
import {GameConnexionTypes} from "../Url/UrlManager";
|
||||
import {iframeListener} from "../Api/IframeListener";
|
||||
|
||||
export type SendMessageCallback = (message:string) => void;
|
||||
|
||||
|
@ -25,6 +26,14 @@ export class DiscussionManager {
|
|||
constructor() {
|
||||
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
|
||||
this.createDiscussPart(''); //todo: why do we always use empty string?
|
||||
|
||||
iframeListener.chatStream.subscribe((chatEvent) => {
|
||||
this.addMessage(chatEvent.author, chatEvent.message, false);
|
||||
this.showDiscussion();
|
||||
});
|
||||
this.onSendMessageCallback('iframe_listener', (message) => {
|
||||
iframeListener.sendUserInputChat(message);
|
||||
})
|
||||
}
|
||||
|
||||
private createDiscussPart(name: string) {
|
||||
|
@ -61,12 +70,12 @@ export class DiscussionManager {
|
|||
const inputMessage: HTMLInputElement = document.createElement('input');
|
||||
inputMessage.onfocus = () => {
|
||||
if(this.userInputManager) {
|
||||
this.userInputManager.clearAllKeys();
|
||||
this.userInputManager.disableControls();
|
||||
}
|
||||
}
|
||||
inputMessage.onblur = () => {
|
||||
if(this.userInputManager) {
|
||||
this.userInputManager.initKeyBoardEvent();
|
||||
this.userInputManager.restoreControls();
|
||||
}
|
||||
}
|
||||
inputMessage.type = "text";
|
||||
|
|
|
@ -24,7 +24,7 @@ export class HtmlUtils {
|
|||
throw new Error("Cannot find HTML element with id '"+id+"'");
|
||||
}
|
||||
|
||||
private static escapeHtml(html: string): string {
|
||||
public static escapeHtml(html: string): string {
|
||||
const text = document.createTextNode(html);
|
||||
const p = document.createElement('p');
|
||||
p.appendChild(text);
|
||||
|
|
232
front/src/iframe_api.ts
Normal file
232
front/src/iframe_api.ts
Normal file
|
@ -0,0 +1,232 @@
|
|||
import {ChatEvent, isChatEvent} from "./Api/Events/ChatEvent";
|
||||
import {isIframeEventWrapper} from "./Api/Events/IframeEvent";
|
||||
import {isUserInputChatEvent, UserInputChatEvent} from "./Api/Events/UserInputChatEvent";
|
||||
import {Subject} from "rxjs";
|
||||
import {EnterLeaveEvent, isEnterLeaveEvent} from "./Api/Events/EnterLeaveEvent";
|
||||
import {OpenPopupEvent} from "./Api/Events/OpenPopupEvent";
|
||||
import {isButtonClickedEvent} from "./Api/Events/ButtonClickedEvent";
|
||||
import {ClosePopupEvent} from "./Api/Events/ClosePopupEvent";
|
||||
import {OpenTabEvent} from "./Api/Events/OpenTabEvent";
|
||||
import {GoToPageEvent} from "./Api/Events/GoToPageEvent";
|
||||
import {OpenCoWebSiteEvent} from "./Api/Events/OpenCoWebSiteEvent";
|
||||
|
||||
interface WorkAdventureApi {
|
||||
sendChatMessage(message: string, author: string): void;
|
||||
onChatMessage(callback: (message: string) => void): void;
|
||||
onEnterZone(name: string, callback: () => void): void;
|
||||
onLeaveZone(name: string, callback: () => void): void;
|
||||
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup;
|
||||
openTab(url : string): void;
|
||||
goToPage(url : string): void;
|
||||
openCoWebSite(url : string): void;
|
||||
closeCoWebSite(): void;
|
||||
disablePlayerControl() : void;
|
||||
restorePlayerControl() : void;
|
||||
displayBubble() : void;
|
||||
removeBubble() : void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var WA: WorkAdventureApi
|
||||
|
||||
}
|
||||
|
||||
type ChatMessageCallback = (message: string) => void;
|
||||
type ButtonClickedCallback = (popup: Popup) => void;
|
||||
|
||||
const userInputChatStream: Subject<UserInputChatEvent> = new Subject();
|
||||
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const popups: Map<number, Popup> = new Map<number, Popup>();
|
||||
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
|
||||
|
||||
let popupId = 0;
|
||||
interface ButtonDescriptor {
|
||||
/**
|
||||
* The label of the button
|
||||
*/
|
||||
label: string,
|
||||
/**
|
||||
* The type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled"
|
||||
*/
|
||||
className?: "normal"|"primary"|"success"|"warning"|"error"|"disabled",
|
||||
/**
|
||||
* Callback called if the button is pressed
|
||||
*/
|
||||
callback: ButtonClickedCallback,
|
||||
}
|
||||
|
||||
class Popup {
|
||||
constructor(private id: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the popup
|
||||
*/
|
||||
public close(): void {
|
||||
window.parent.postMessage({
|
||||
'type': 'closePopup',
|
||||
'data': {
|
||||
'popupId': this.id,
|
||||
} as ClosePopupEvent
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
window.WA = {
|
||||
/**
|
||||
* Send a message in the chat.
|
||||
* Only the local user will receive this message.
|
||||
*/
|
||||
sendChatMessage(message: string, author: string) {
|
||||
window.parent.postMessage({
|
||||
'type': 'chat',
|
||||
'data': {
|
||||
'message': message,
|
||||
'author': author
|
||||
} as ChatEvent
|
||||
}, '*');
|
||||
},
|
||||
disablePlayerControl() : void {
|
||||
window.parent.postMessage({'type' : 'disablePlayerControl'},'*');
|
||||
},
|
||||
|
||||
restorePlayerControl() : void {
|
||||
window.parent.postMessage({'type' : 'restorePlayerControl'},'*');
|
||||
},
|
||||
|
||||
displayBubble() : void {
|
||||
window.parent.postMessage({'type' : 'displayBubble'},'*');
|
||||
},
|
||||
|
||||
removeBubble() : void {
|
||||
window.parent.postMessage({'type' : 'removeBubble'},'*');
|
||||
},
|
||||
|
||||
openTab(url : string) : void{
|
||||
window.parent.postMessage({
|
||||
"type" : 'openTab',
|
||||
"data" : {
|
||||
url
|
||||
} as OpenTabEvent
|
||||
},'*');
|
||||
},
|
||||
|
||||
goToPage(url : string) : void{
|
||||
window.parent.postMessage({
|
||||
"type" : 'goToPage',
|
||||
"data" : {
|
||||
url
|
||||
} as GoToPageEvent
|
||||
},'*');
|
||||
},
|
||||
|
||||
openCoWebSite(url : string) : void{
|
||||
window.parent.postMessage({
|
||||
"type" : 'openCoWebSite',
|
||||
"data" : {
|
||||
url
|
||||
} as OpenCoWebSiteEvent
|
||||
},'*');
|
||||
},
|
||||
|
||||
closeCoWebSite() : void{
|
||||
window.parent.postMessage({
|
||||
"type" : 'closeCoWebSite'
|
||||
},'*');
|
||||
},
|
||||
|
||||
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
|
||||
popupId++;
|
||||
const popup = new Popup(popupId);
|
||||
const btnMap = new Map<number, () => void>();
|
||||
popupCallbacks.set(popupId, btnMap);
|
||||
let id = 0;
|
||||
for (const button of buttons) {
|
||||
const callback = button.callback;
|
||||
if (callback) {
|
||||
btnMap.set(id, () => {
|
||||
callback(popup);
|
||||
});
|
||||
}
|
||||
id++;
|
||||
}
|
||||
|
||||
|
||||
window.parent.postMessage({
|
||||
'type': 'openPopup',
|
||||
'data': {
|
||||
popupId,
|
||||
targetObject,
|
||||
message,
|
||||
buttons: buttons.map((button) => {
|
||||
return {
|
||||
label: button.label,
|
||||
className: button.className
|
||||
};
|
||||
})
|
||||
} as OpenPopupEvent
|
||||
}, '*');
|
||||
|
||||
popups.set(popupId, popup)
|
||||
return popup;
|
||||
},
|
||||
/**
|
||||
* Listen to messages sent by the local user, in the chat.
|
||||
*/
|
||||
onChatMessage(callback: ChatMessageCallback): void {
|
||||
userInputChatStream.subscribe((userInputChatEvent) => {
|
||||
callback(userInputChatEvent.message);
|
||||
});
|
||||
},
|
||||
onEnterZone(name: string, callback: () => void): void {
|
||||
let subject = enterStreams.get(name);
|
||||
if (subject === undefined) {
|
||||
subject = new Subject<EnterLeaveEvent>();
|
||||
enterStreams.set(name, subject);
|
||||
}
|
||||
subject.subscribe(callback);
|
||||
},
|
||||
onLeaveZone(name: string, callback: () => void): void {
|
||||
let subject = leaveStreams.get(name);
|
||||
if (subject === undefined) {
|
||||
subject = new Subject<EnterLeaveEvent>();
|
||||
leaveStreams.set(name, subject);
|
||||
}
|
||||
subject.subscribe(callback);
|
||||
},
|
||||
}
|
||||
|
||||
window.addEventListener('message', message => {
|
||||
if (message.source !== window.parent) {
|
||||
return; // Skip message in this event listener
|
||||
}
|
||||
|
||||
const payload = message.data;
|
||||
|
||||
console.log(payload);
|
||||
|
||||
if (isIframeEventWrapper(payload)) {
|
||||
const payloadData = payload.data;
|
||||
if (payload.type === 'userInputChat' && isUserInputChatEvent(payloadData)) {
|
||||
userInputChatStream.next(payloadData);
|
||||
} else if (payload.type === 'enterEvent' && isEnterLeaveEvent(payloadData)) {
|
||||
enterStreams.get(payloadData.name)?.next();
|
||||
} else if (payload.type === 'leaveEvent' && isEnterLeaveEvent(payloadData)) {
|
||||
leaveStreams.get(payloadData.name)?.next();
|
||||
} else if (payload.type === 'buttonClickedEvent' && isButtonClickedEvent(payloadData)) {
|
||||
const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId);
|
||||
const popup = popups.get(payloadData.popupId);
|
||||
if (popup === undefined) {
|
||||
throw new Error('Could not find popup with ID "'+payloadData.popupId+'"');
|
||||
}
|
||||
if (callback) {
|
||||
callback(popup);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ...
|
||||
});
|
|
@ -15,6 +15,8 @@ import {MenuScene} from "./Phaser/Menu/MenuScene";
|
|||
import {HelpCameraSettingsScene} from "./Phaser/Menu/HelpCameraSettingsScene";
|
||||
import {localUserStore} from "./Connexion/LocalUserStore";
|
||||
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
|
||||
import {iframeListener} from "./Api/IframeListener";
|
||||
import {discussionManager} from "./WebRtc/DiscussionManager";
|
||||
|
||||
const {width, height} = coWebsiteManager.getGameSize();
|
||||
|
||||
|
@ -119,3 +121,5 @@ coWebsiteManager.onResize.subscribe(() => {
|
|||
const {width, height} = coWebsiteManager.getGameSize();
|
||||
game.scale.resize(width / RESOLUTION, height / RESOLUTION);
|
||||
});
|
||||
|
||||
iframeListener.init();
|
||||
|
|
|
@ -4,7 +4,10 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.ts',
|
||||
entry: {
|
||||
'main': './src/index.ts',
|
||||
'iframe_api': './src/iframe_api.ts'
|
||||
},
|
||||
devtool: 'inline-source-map',
|
||||
devServer: {
|
||||
contentBase: './dist',
|
||||
|
@ -34,7 +37,11 @@ module.exports = {
|
|||
extensions: [ '.tsx', '.ts', '.js' ],
|
||||
},
|
||||
output: {
|
||||
filename: '[name].[contenthash].js',
|
||||
filename: (pathData) => {
|
||||
// Add a content hash only for the main bundle.
|
||||
// We want the iframe_api.js file to keep its name as it will be referenced from outside iframes.
|
||||
return pathData.chunk.name === 'main' ? 'js/[name].[contenthash].js': '[name].js';
|
||||
},
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '/'
|
||||
},
|
||||
|
@ -54,7 +61,8 @@ module.exports = {
|
|||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
useShortDoctype: true
|
||||
}
|
||||
},
|
||||
chunks: ['main']
|
||||
}
|
||||
),
|
||||
new webpack.ProvidePlugin({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue