Refactor to only have one function registerMenuCommand

When selected custom menu is removed, go to settings menu
Allow iframe in custom menu to use Scripting API
Return menu object when it is registered, can call remove function on it
This commit is contained in:
GRL 2021-08-27 10:34:03 +02:00
parent 5cd3ab4b4c
commit cf7bfe79ca
14 changed files with 203 additions and 87 deletions

View file

@ -32,7 +32,7 @@ import type {
TriggerActionMessageEvent,
} from "./ui/TriggerActionMessageEvent";
import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
import type { MenuIframeRegisterEvent, MenuItemRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T;
@ -63,8 +63,7 @@ export type IframeEventMap = {
stopSound: null;
getState: undefined;
loadTileset: LoadTilesetEvent;
registerMenuCommand: MenuItemRegisterEvent;
registerMenuIframe: MenuIframeRegisterEvent;
registerMenu: MenuRegisterEvent;
unregisterMenu: UnregisterMenuEvent;
setTiles: SetTilesEvent;
modifyEmbeddedWebsite: Partial<EmbeddedWebsite>; // Note: name should be compulsory in fact

View file

@ -1,5 +1,4 @@
import * as tg from "generic-type-guard";
import { isMenuItemRegisterEvent } from "./ui/MenuRegisterEvent";
export const isSetVariableEvent = new tg.IsInterface()
.withProperties({

View file

@ -1,35 +1,5 @@
import * as tg from "generic-type-guard";
/**
* A message sent from a script to the game to add a new button in the menu.
*/
export const isMenuItemRegisterEvent = new tg.IsInterface()
.withProperties({
menuItem: tg.isString,
})
.get();
export type MenuItemRegisterEvent = tg.GuardedType<typeof isMenuItemRegisterEvent>;
export const isMenuItemRegisterIframeEvent = new tg.IsInterface()
.withProperties({
type: tg.isSingletonString("registerMenuCommand"),
data: isMenuItemRegisterEvent,
})
.get();
/**
* A message sent from a script to the game to add an iframe submenu in the menu.
*/
export const isMenuIframeEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
url: tg.isString,
})
.get();
export type MenuIframeRegisterEvent = tg.GuardedType<typeof isMenuIframeEvent>;
/**
* A message sent from a script to the game to remove a custom menu from the menu
*/
@ -40,3 +10,22 @@ export const isUnregisterMenuEvent = new tg.IsInterface()
.get();
export type UnregisterMenuEvent = tg.GuardedType<typeof isUnregisterMenuEvent>;
export const isMenuRegisterOptions = new tg.IsInterface()
.withProperties({
allowApi: tg.isBoolean,
})
.get();
/**
* A message sent from a script to the game to add a custom menu from the menu
*/
export const isMenuRegisterEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
iframe: tg.isUnion(tg.isString, tg.isUndefined),
options: isMenuRegisterOptions,
})
.get();
export type MenuRegisterEvent = tg.GuardedType<typeof isMenuRegisterEvent>;

View file

@ -29,7 +29,7 @@ import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent"
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { isMenuItemRegisterIframeEvent, isMenuIframeEvent, isUnregisterMenuEvent } from "./Events/ui/MenuRegisterEvent";
import { isMenuRegisterEvent, isUnregisterMenuEvent } from "./Events/ui/MenuRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
import type { SetVariableEvent } from "./Events/SetVariableEvent";
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
@ -255,22 +255,21 @@ class IframeListener {
this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menuItem;
this.iframeCloseCallbacks.get(iframe)?.push(() => {
handleMenuUnregisterEvent(data);
});
handleMenuRegistrationEvent(payload.data.menuItem);
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data);
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
this._modifyEmbeddedWebsiteStream.next(payload.data);
} else if (payload.type == "registerMenuIframe" && isMenuIframeEvent(payload.data)) {
const data = payload.data.name;
} else if (payload.type == "registerMenu" && isMenuRegisterEvent(payload.data)) {
const dataName = payload.data.name;
this.iframeCloseCallbacks.get(iframe)?.push(() => {
handleMenuUnregisterEvent(data);
handleMenuUnregisterEvent(dataName);
});
handleMenuRegistrationEvent(payload.data.name, payload.data.url, foundSrc);
handleMenuRegistrationEvent(
payload.data.name,
payload.data.iframe,
foundSrc,
payload.data.options
);
} else if (payload.type == "unregisterMenu" && isUnregisterMenuEvent(payload.data)) {
handleMenuUnregisterEvent(payload.data.name);
}

View file

@ -0,0 +1,17 @@
import { sendToWorkadventure } from "../IframeApiContribution";
export class Menu {
constructor(private menuName: string) {}
/**
* remove the menu
*/
public remove() {
sendToWorkadventure({
type: "unregisterMenu",
data: {
name: this.menuName,
},
});
}
}

View file

@ -6,6 +6,8 @@ import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescrip
import { Popup } from "./Ui/Popup";
import { ActionMessage } from "./Ui/ActionMessage";
import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent";
import { Menu } from "./Ui/Menu";
import type { RequireOnlyOne } from "../../types";
let popupId = 0;
const popups: Map<number, Popup> = new Map<number, Popup>();
@ -14,9 +16,18 @@ const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<
Map<number, ButtonClickedCallback>
>();
const menus: Map<string, Menu> = new Map<string, Menu>();
const menuCallbacks: Map<string, (command: string) => void> = new Map();
const actionMessages = new Map<string, ActionMessage>();
interface MenuDescriptor {
callback?: (commandDescriptor: string) => void;
iframe?: string;
allowApi?: boolean;
}
type CallbackOrIframe = RequireOnlyOne<MenuDescriptor, "callback" | "iframe">;
interface ZonedPopupOptions {
zone: string;
objectLayerName?: string;
@ -52,6 +63,10 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
typeChecker: isMenuItemClickedEvent,
callback: (event) => {
const callback = menuCallbacks.get(event.menuItem);
const menu = menus.get(event.menuItem);
if (menu === undefined) {
throw new Error('Could not find menu named "' + event.menuItem + '"');
}
if (callback) {
callback(event.menuItem);
}
@ -104,36 +119,56 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
return popup;
}
registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
menuCallbacks.set(commandDescriptor, callback);
sendToWorkadventure({
type: "registerMenuCommand",
data: {
menuItem: commandDescriptor,
},
});
}
registerMenuCommand(
commandDescriptor: string,
options: CallbackOrIframe | ((commandDescriptor: string) => void)
): Menu {
const menu = new Menu(commandDescriptor);
registerMenuIframe(menuName: string, iframeUrl: string) {
sendToWorkadventure({
type: "registerMenuIframe",
data: {
name: menuName,
url: iframeUrl,
},
});
}
if (typeof options === "function") {
menuCallbacks.set(commandDescriptor, options);
sendToWorkadventure({
type: "registerMenu",
data: {
name: commandDescriptor,
options: {
allowApi: false,
},
},
});
} else {
options.allowApi = options.allowApi === undefined ? options.iframe !== undefined : options.allowApi;
unregisterMenu(menuName: string) {
sendToWorkadventure({
type: "unregisterMenu",
data: {
name: menuName,
},
});
if (menuCallbacks.get(menuName)) {
menuCallbacks.delete(menuName);
if (options.iframe !== undefined) {
sendToWorkadventure({
type: "registerMenu",
data: {
name: commandDescriptor,
iframe: options.iframe,
options: {
allowApi: options.allowApi,
},
},
});
} else if (options.callback !== undefined) {
menuCallbacks.set(commandDescriptor, options.callback);
sendToWorkadventure({
type: "registerMenu",
data: {
name: commandDescriptor,
options: {
allowApi: options.allowApi,
},
},
});
} else {
throw new Error(
"When adding a menu with WA.ui.registerMenuCommand, you must pass either an iframe or a callback"
);
}
}
menus.set(commandDescriptor, menu);
return menu;
}
displayBubble(): void {

View file

@ -1,10 +1,27 @@
<script lang="ts">
export let iframe: string;
import {onDestroy, onMount} from "svelte";
import {iframeListener} from "../../Api/IframeListener";
export let url: string;
export let allowApi: boolean;
let HTMLIframe: HTMLIFrameElement;
onMount( () => {
if (allowApi) {
iframeListener.registerIframe(HTMLIframe);
}
})
onDestroy( () => {
if (allowApi) {
iframeListener.unregisterIframe(HTMLIframe);
}
})
</script>
<iframe title="customSubMenu" src="{iframe}"></iframe>
<iframe title="customSubMenu" src="{url}" bind:this={HTMLIframe}></iframe>
<style lang="scss">
iframe {

View file

@ -9,14 +9,15 @@
import CustomSubMenu from "./CustomSubMenu.svelte"
import {customMenuIframe, menuVisiblilityStore, SubMenusInterface, subMenusStore} from "../../Stores/MenuStore";
import {userIsAdminStore} from "../../Stores/GameStore";
import {onMount} from "svelte";
import {get} from "svelte/store";
import {onDestroy, onMount} from "svelte";
import {get, Unsubscriber} from "svelte/store";
import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem";
import {CONTACT_URL} from "../../Enum/EnvironmentVariable";
let activeSubMenu: string = SubMenusInterface.settings;
let activeComponent: typeof SettingsSubMenu | typeof CustomSubMenu= SettingsSubMenu;
let props: {iframe: string};
let activeComponent: typeof SettingsSubMenu | typeof CustomSubMenu = SettingsSubMenu;
let props: { url: string, allowApi: boolean };
let unsubscriberSubMenuStore: Unsubscriber;
onMount(() => {
if(!get(userIsAdminStore)) {
@ -27,9 +28,21 @@
subMenusStore.removeMenu(SubMenusInterface.contact);
}
unsubscriberSubMenuStore = subMenusStore.subscribe(() => {
if(!get(subMenusStore).includes(activeSubMenu)) {
switchMenu(SubMenusInterface.settings);
}
})
switchMenu(SubMenusInterface.settings);
})
onDestroy(() => {
if(unsubscriberSubMenuStore) {
unsubscriberSubMenuStore();
}
})
function switchMenu(menu: string) {
if (get(subMenusStore).find((subMenu) => subMenu === menu)) {
activeSubMenu = menu;
@ -55,7 +68,7 @@
default:
const customMenu = customMenuIframe.get(menu);
if (customMenu !== undefined) {
props = {iframe: customMenu};
props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu;
} else {
sendMenuClickedEvent(menu);

View file

@ -67,12 +67,13 @@ function createSubMenusStore() {
export const subMenusStore = createSubMenusStore();
export const customMenuIframe = new Map<string, string>();
export const customMenuIframe = new Map<string, { url: string; allowApi: boolean }>();
export function handleMenuRegistrationEvent(
menuName: string,
iframeUrl: string | undefined = undefined,
source: string | undefined = undefined
source: string | undefined = undefined,
options: { allowApi: boolean }
) {
if (get(subMenusStore).includes(menuName)) {
console.warn("The menu " + menuName + " already exist.");
@ -82,9 +83,8 @@ export function handleMenuRegistrationEvent(
subMenusStore.addMenu(menuName);
if (iframeUrl !== undefined) {
//Add a subMenu from an iframe to the menu
const url = new URL(iframeUrl, source);
customMenuIframe.set(menuName, url.toString());
customMenuIframe.set(menuName, { url: url.toString(), allowApi: options.allowApi });
}
}

View file

@ -1,10 +1,10 @@
import type Phaser from "phaser";
export type CursorKey = {
isDown: boolean
}
isDown: boolean;
};
export type Direction = 'left' | 'right' | 'up' | 'down'
export type Direction = "left" | "right" | "up" | "down";
export interface CursorKeys extends Record<Direction, CursorKey> {
left: CursorKey;
@ -22,3 +22,7 @@ export interface IVirtualJoystick extends Phaser.GameObjects.GameObject {
createCursorKeys: () => CursorKeys;
}
export type RequireOnlyOne<T, keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, keys>> &
{
[K in keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<keys, K>, undefined>>;
}[keys];

View file

@ -0,0 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<title>Custom Iframe Menu</title>
</head>
<body>
<a href="https://workadventu.re/">GO TO WA BY IFRAME</a>
</body>
</html>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>API in iframe menu</title>
<script>
var script = document.createElement('script');
// Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security.
// We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files.
script.setAttribute('src', document.referrer + 'iframe_api.js');
document.head.appendChild(script);
window.addEventListener('load', () => {
WA.chat.sendChatMessage('The iframe opened by a script works !', 'Mr Robot');
})
</script>
</head>
<body>
<p>This iframe send you a message in the chat.</p>
</body>
</html>

View file

@ -8,7 +8,7 @@
script.setAttribute('src', document.referrer + 'iframe_api.js');
document.head.appendChild(script);
window.addEventListener('load', () => {
WA.ui.registerMenuIframe('test', 'customIframeMenu.html');
WA.ui.registerMenuCommand('test', 'customIframeMenu.html', {autoClose: true});
})
</script>
</head>

View file

@ -0,0 +1,15 @@
let menuIframeApi = undefined;
WA.ui.registerMenuCommand('TO WA', () => {
WA.nav.openTab("https://workadventu.re/");
})
WA.ui.registerMenuCommand('TO WA BY IFRAME', {iframe: 'customIframeMenu.html'});
WA.room.onEnterZone('iframeMenu', () => {
menuIframeApi = WA.ui.registerMenuCommand('IFRAME USE API', {iframe: 'customIframeMenuApi.html', allowApi: true});
})
WA.room.onLeaveZone('iframeMenu', () => {
menuIframeApi.remove();
})