diff --git a/front/src/Api/iframe/IframeApiContribution.ts b/front/src/Api/iframe/IframeApiContribution.ts new file mode 100644 index 00000000..ee2ecff5 --- /dev/null +++ b/front/src/Api/iframe/IframeApiContribution.ts @@ -0,0 +1,48 @@ +import type { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent'; +import type * as tg from "generic-type-guard"; + + +export type PossibleSubobjects = "zone" | "chat" | "ui" + +export function sendToWorkadventure(content: IframeEvent) { + window.parent.postMessage(content, "*") +} +type GuardedType> = Guard extends tg.TypeGuard ? T : never + +export function apiCallback>(callbackData: IframeCallbackContribution) { + + return callbackData +} + +export interface IframeCallbackContribution, T = GuardedType> { + + type: keyof IframeResponseEventMap, + typeChecker: Guard, + callback: (payloadData: T) => void +} + + +/** + * !! be aware that the implemented attributes (addMethodsAtRoot and subObjectIdentifier) must be readonly + * + * + */ + +export abstract class IframeApiContribution>>, + readonly subObjectIdentifier: PossibleSubobjects, + readonly addMethodsAtRoot: boolean | undefined +}> { + + abstract callbacks: T["callbacks"] + + /** + * @deprecated this is only there for backwards compatibility on new apis this should be set to false or ignored + */ + addMethodsAtRoot = false + + abstract readonly subObjectIdentifier: T["subObjectIdentifier"] + +} \ No newline at end of file diff --git a/front/src/Api/iframe/chatmessage.ts b/front/src/Api/iframe/chatmessage.ts index 934fb1b1..7f4f045a 100644 --- a/front/src/Api/iframe/chatmessage.ts +++ b/front/src/Api/iframe/chatmessage.ts @@ -1,10 +1,24 @@ import { ChatEvent } from '../Events/ChatEvent' import { isUserInputChatEvent, UserInputChatEvent } from '../Events/UserInputChatEvent' -import { registerWorkadventureCommand, registerWorkadvntureCallback, sendToWorkadventure } from "./iframe-registration" +import { } from "./iframe-registration" +import { apiCallback, IframeApiContribution, sendToWorkadventure } from './IframeApiContribution' -let chatMessageCallback: (event: string) => void | undefined -class WorkadvntureChatCommands { +class WorkadvntureChatCommands extends IframeApiContribution { + readonly subObjectIdentifier = 'chat' + + readonly addMethodsAtRoot = true + + chatMessageCallback?: (event: string) => void + + callbacks = [apiCallback({ + callback: (event: UserInputChatEvent) => { + this.chatMessageCallback?.(event.message) + }, + type: "userInputChat", + typeChecker: isUserInputChatEvent + })] + sendChatMessage(message: string, author: string) { sendToWorkadventure({ @@ -20,16 +34,8 @@ class WorkadvntureChatCommands { * Listen to messages sent by the local user, in the chat. */ onChatMessage(callback: (message: string) => void) { - chatMessageCallback = callback + this.chatMessageCallback = callback } } -export const commands = registerWorkadventureCommand(new WorkadvntureChatCommands()) -export const callbacks = registerWorkadvntureCallback([{ - callback: (event: UserInputChatEvent) => { - chatMessageCallback?.(event.message) - }, - type: "userInputChat", - typeChecker: isUserInputChatEvent -}]) - +export default new WorkadvntureChatCommands() \ No newline at end of file diff --git a/front/src/Api/iframe/iframe-registration.ts b/front/src/Api/iframe/iframe-registration.ts index ceb6daf4..ea9ce5ad 100644 --- a/front/src/Api/iframe/iframe-registration.ts +++ b/front/src/Api/iframe/iframe-registration.ts @@ -1,6 +1,6 @@ import { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent'; import { registeredCallbacks, WorkAdventureApi } from "../../iframe_api" -export function registerWorkadventureCommand(commnds: T): T { +/*export function registerWorkadventureCommand(commnds: T): T { const commandPrototype = Object.getPrototypeOf(commnds); const commandClassPropertyNames = Object.getOwnPropertyNames(commandPrototype).filter(name => name !== "constructor"); for (const key of commandClassPropertyNames) { @@ -8,7 +8,7 @@ export function registerWorkadventureCommand(commnds: T): T { } return commnds } - +*/ export function registerWorkadvntureCallback(callbacks: Array<{ type: keyof IframeResponseEventMap, @@ -25,6 +25,3 @@ export function registerWorkadvntureCallback(callbacks: Arra } -export function sendToWorkadventure(content: IframeEvent) { - window.parent.postMessage(content, "*") -} \ No newline at end of file diff --git a/front/src/Api/iframe/popup.ts b/front/src/Api/iframe/popup.ts new file mode 100644 index 00000000..72ee87c7 --- /dev/null +++ b/front/src/Api/iframe/popup.ts @@ -0,0 +1,148 @@ +import { isButtonClickedEvent } from '../Events/ButtonClickedEvent'; +import { ClosePopupEvent } from '../Events/ClosePopupEvent'; +import { apiCallback, IframeApiContribution, IframeCallbackContribution, sendToWorkadventure } from './IframeApiContribution'; +import zoneCommands from "./zone-events" +class Popup { + constructor(private id: number) { + } + + /** + * Closes the popup + */ + public close(): void { + window.parent.postMessage({ + 'type': 'closePopup', + 'data': { + 'popupId': this.id, + } as ClosePopupEvent + }, '*'); + } +} + +type ButtonClickedCallback = (popup: Popup) => void; +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, +} +let popupId = 0; +const popups: Map = new Map(); +const popupCallbacks: Map> = new Map>(); + +interface ZonedPopupOptions { + zone: string + objectLayerName?: string, + popupText: string, + delay?: number + popupOptions: Array +} + + +class PopupApiContribution extends IframeApiContribution { + + readonly subObjectIdentifier = "ui" + + readonly addMethodsAtRoot = true + callbacks = [apiCallback({ + type: "buttonClickedEvent", + typeChecker: isButtonClickedEvent, + callback: (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); + } + } + })]; + + + openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup { + popupId++; + const popup = new Popup(popupId); + const btnMap = new Map 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++; + } + + sendToWorkadventure({ + 'type': 'openPopup', + 'data': { + popupId, + targetObject, + message, + buttons: buttons.map((button) => { + return { + label: button.label, + className: button.className + }; + }) + } + }); + + popups.set(popupId, popup) + return popup; + } + + + + popupInZone(options: ZonedPopupOptions) { + const objectLayerName = options.objectLayerName || options.zone + + let lastOpened = 0; + + let popup: Popup | undefined; + zoneCommands.onEnterZone(options.zone, () => { + if (options.delay) { + if (lastOpened + options.delay > Date.now()) { + return; + } + } + lastOpened = Date.now(); + popup = this.openPopup(objectLayerName, options.popupText, options.popupOptions.map(option => { + const callback = option.callback; + const popupOptions = { + ...option, + className: option.className || 'normal', + callback: () => { + if (callback && popup) { + callback(popup); + } + popup?.close(); + popup = undefined; + } + }; + + return popupOptions; + })); + }); + zoneCommands.onLeaveZone(options.zone, () => { + if (popup) { + popup.close(); + popup = undefined; + } + }); + } + +} + +export default new PopupApiContribution() \ No newline at end of file diff --git a/front/src/Api/iframe/zone-events.ts b/front/src/Api/iframe/zone-events.ts index 11df18d9..8f66b45a 100644 --- a/front/src/Api/iframe/zone-events.ts +++ b/front/src/Api/iframe/zone-events.ts @@ -1,27 +1,54 @@ import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent' -import { registerWorkadventureCommand, registerWorkadvntureCallback, sendToWorkadventure } from "./iframe-registration" +import { apiCallback as apiCallback, IframeApiContribution } from './IframeApiContribution' +import { Subject } from "rxjs"; + + +const enterStreams: Map> = new Map>(); +const leaveStreams: Map> = new Map>(); + +class WorkadventureZoneCommands extends IframeApiContribution { + + readonly subObjectIdentifier = "zone" + + readonly addMethodsAtRoot = true + callbacks = [ + apiCallback({ + callback: (payloadData: EnterLeaveEvent) => { + enterStreams.get(payloadData.name)?.next(); + }, + type: "enterEvent", + typeChecker: isEnterLeaveEvent + }), + apiCallback({ + type: "leaveEvent", + typeChecker: isEnterLeaveEvent, + callback: (payloadData) => { + leaveStreams.get(payloadData.name)?.next(); + } + }) + + ] -class WorkadventureZoneCommands { onEnterZone(name: string, callback: () => void): void { - + let subject = enterStreams.get(name); + if (subject === undefined) { + subject = new Subject(); + enterStreams.set(name, subject); + } + subject.subscribe(callback); } onLeaveZone(name: string, callback: () => void): void { - + let subject = leaveStreams.get(name); + if (subject === undefined) { + subject = new Subject(); + leaveStreams.set(name, subject); + } + subject.subscribe(callback); } } - - -export const commands = registerWorkadventureCommand(new WorkadventureZoneCommands()) -export const callbacks = registerWorkadvntureCallback([{ - callback: (enterEvent: EnterLeaveEvent) => { - - }, - type: "enterEvent", - typeChecker: isEnterLeaveEvent -},]) - +export default new WorkadventureZoneCommands(); \ No newline at end of file diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts index fbf3ec4f..b3126cbb 100644 --- a/front/src/iframe_api.ts +++ b/front/src/iframe_api.ts @@ -9,23 +9,54 @@ import type { ClosePopupEvent } from "./Api/Events/ClosePopupEvent"; import type { OpenTabEvent } from "./Api/Events/OpenTabEvent"; import type { GoToPageEvent } from "./Api/Events/GoToPageEvent"; import type { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent"; +import { OpenTabEvent } from "./Api/Events/OpenTabEvent"; +import { GoToPageEvent } from "./Api/Events/GoToPageEvent"; +import { OpenCoWebSiteEvent, OpenCoWebSiteOptionsEvent } from "./Api/Events/OpenCoWebSiteEvent"; +import { LoadPageEvent } from './Api/Events/LoadPageEvent'; +import { isMenuItemClickedEvent } from './Api/Events/MenuItemClickedEvent'; +import { MenuItemRegisterEvent } from './Api/Events/MenuItemRegisterEvent'; +import { GameStateEvent, isGameStateEvent } from './Api/Events/ApiGameStateEvent'; +import { updateTile, UpdateTileEvent } from './Api/Events/ApiUpdateTileEvent'; +import { isMessageReferenceEvent, removeTriggerMessage, triggerMessage, TriggerMessageCallback, TriggerMessageEvent } from './Api/Events/TriggerMessageEvent'; +import { HasMovedEvent, HasMovedEventCallback, isHasMovedEvent } from './Api/Events/HasMovedEvent'; const importType = Promise.all([ + import("./Api/iframe/popup"), import("./Api/iframe/chatmessage"), import("./Api/iframe/zone-events") ]) -type UnPromise

= P extends Promise ? T : P -type WorkadventureCommandClasses = UnPromise[number]["commands"]; +type PromiseReturnType

= P extends Promise ? T : P + +type WorkadventureCommandClasses = PromiseReturnType[number]["default"]; + type KeysOfUnion = T extends T ? keyof T : never -type ObjectWithKeyOfUnion = O extends O ? (Key extends keyof O ? O[Key] : never) : never -type WorkAdventureApiFiles = { [Key in KeysOfUnion]: ObjectWithKeyOfUnion }; +type ObjectWithKeyOfUnion = O extends O ? (Key extends keyof O ? O[Key] : never) : never + +type ApiKeys = KeysOfUnion; + +type ObjectOfKey = O extends O ? (Key extends keyof O ? O : never) : never + +type ShouldAddAttribute = ObjectWithKeyOfUnion; + +type WorkadventureFunctions = { [K in ApiKeys]: ObjectWithKeyOfUnion extends Function ? K : never }[ApiKeys] + +type WorkadventureFunctionsFilteredByRoot = { [K in WorkadventureFunctions]: ObjectOfKey["addMethodsAtRoot"] extends true ? K : never }[WorkadventureFunctions] + + +type JustMethodKeys = ({ [P in keyof T]: T[P] extends Function ? P : never })[keyof T]; +type JustMethods = Pick>; + +type SubObjectTypes = { + [importCl in WorkadventureCommandClasses as importCl["subObjectIdentifier"]]: JustMethods; +}; + +type WorkAdventureApiFiles = { + [Key in WorkadventureFunctionsFilteredByRoot]: ShouldAddAttribute +} & SubObjectTypes export interface WorkAdventureApi extends WorkAdventureApiFiles { - 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; @@ -47,47 +78,11 @@ declare global { let WA: WorkAdventureApi } -type ChatMessageCallback = (message: string) => void; -type ButtonClickedCallback = (popup: Popup) => void; - const userInputChatStream: Subject = new Subject(); -const enterStreams: Map> = new Map>(); -const leaveStreams: Map> = new Map>(); -const popups: Map = new Map(); -const popupCallbacks: Map> = new Map>(); -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 = { /** @@ -152,59 +147,16 @@ window.WA = { }, '*'); }, - openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup { - popupId++; - const popup = new Popup(popupId); - const btnMap = new Map 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++; - } - - + registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) { + menuCallbacks.set(commandDescriptor, callback); window.parent.postMessage({ - 'type': 'openPopup', + 'type': 'registerMenuCommand', 'data': { - popupId, - targetObject, - message, - buttons: buttons.map((button) => { - return { - label: button.label, - className: button.className - }; - }) - } as OpenPopupEvent + menutItem: commandDescriptor + } as MenuItemRegisterEvent }, '*'); - - popups.set(popupId, popup) - return popup; }, ...({} as WorkAdventureApiFiles), - onEnterZone(name: string, callback: () => void): void { - let subject = enterStreams.get(name); - if (subject === undefined) { - subject = new Subject(); - enterStreams.set(name, subject); - } - subject.subscribe(callback); - }, - onLeaveZone(name: string, callback: () => void): void { - let subject = leaveStreams.get(name); - if (subject === undefined) { - subject = new Subject(); - leaveStreams.set(name, subject); - } - subject.subscribe(callback); - }, - } window.addEventListener('message', message => { @@ -226,22 +178,9 @@ window.addEventListener('message', message => { 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); - } } } // ... -}); +}); \ No newline at end of file