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:
parent
5cd3ab4b4c
commit
cf7bfe79ca
@ -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
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
import { isMenuItemRegisterEvent } from "./ui/MenuRegisterEvent";
|
||||
|
||||
export const isSetVariableEvent = new tg.IsInterface()
|
||||
.withProperties({
|
||||
|
@ -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>;
|
||||
|
@ -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);
|
||||
}
|
||||
|
17
front/src/Api/iframe/Ui/Menu.ts
Normal file
17
front/src/Api/iframe/Ui/Menu.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
|
9
maps/tests/Metadata/customIframeMenu.html
Normal file
9
maps/tests/Metadata/customIframeMenu.html
Normal 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>
|
20
maps/tests/Metadata/customIframeMenuApi.html
Normal file
20
maps/tests/Metadata/customIframeMenuApi.html
Normal 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>
|
@ -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>
|
||||
|
15
maps/tests/Metadata/customMenu.js
Normal file
15
maps/tests/Metadata/customMenu.js
Normal 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();
|
||||
})
|
Loading…
Reference in New Issue
Block a user