added typedef for subobject types

extracted popup functions

# Conflicts:
#	front/package-lock.json
#	front/package.json
#	front/src/iframe_api.ts
This commit is contained in:
jonny 2021-05-28 00:24:08 +02:00
parent 1a1ab30574
commit 2de2d114a1
6 changed files with 304 additions and 139 deletions

View File

@ -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<keyof IframeEventMap>) {
window.parent.postMessage(content, "*")
}
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
export function apiCallback<T extends tg.TypeGuard<unknown>>(callbackData: IframeCallbackContribution<T>) {
return callbackData
}
export interface IframeCallbackContribution<Guard extends tg.TypeGuard<unknown>, T = GuardedType<Guard>> {
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<T extends {
// i think this is specific enough
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callbacks: Array<IframeCallbackContribution<tg.TypeGuard<any>>>,
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"]
}

View File

@ -1,10 +1,24 @@
import { ChatEvent } from '../Events/ChatEvent' import { ChatEvent } from '../Events/ChatEvent'
import { isUserInputChatEvent, UserInputChatEvent } from '../Events/UserInputChatEvent' 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<WorkadvntureChatCommands> {
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) { sendChatMessage(message: string, author: string) {
sendToWorkadventure({ sendToWorkadventure({
@ -20,16 +34,8 @@ class WorkadvntureChatCommands {
* Listen to messages sent by the local user, in the chat. * Listen to messages sent by the local user, in the chat.
*/ */
onChatMessage(callback: (message: string) => void) { onChatMessage(callback: (message: string) => void) {
chatMessageCallback = callback this.chatMessageCallback = callback
} }
} }
export const commands = registerWorkadventureCommand(new WorkadvntureChatCommands()) export default new WorkadvntureChatCommands()
export const callbacks = registerWorkadvntureCallback([{
callback: (event: UserInputChatEvent) => {
chatMessageCallback?.(event.message)
},
type: "userInputChat",
typeChecker: isUserInputChatEvent
}])

View File

@ -1,6 +1,6 @@
import { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent'; import { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent';
import { registeredCallbacks, WorkAdventureApi } from "../../iframe_api" import { registeredCallbacks, WorkAdventureApi } from "../../iframe_api"
export function registerWorkadventureCommand<T>(commnds: T): T { /*export function registerWorkadventureCommand<T>(commnds: T): T {
const commandPrototype = Object.getPrototypeOf(commnds); const commandPrototype = Object.getPrototypeOf(commnds);
const commandClassPropertyNames = Object.getOwnPropertyNames(commandPrototype).filter(name => name !== "constructor"); const commandClassPropertyNames = Object.getOwnPropertyNames(commandPrototype).filter(name => name !== "constructor");
for (const key of commandClassPropertyNames) { for (const key of commandClassPropertyNames) {
@ -8,7 +8,7 @@ export function registerWorkadventureCommand<T>(commnds: T): T {
} }
return commnds return commnds
} }
*/
export function registerWorkadvntureCallback<T extends Function>(callbacks: Array<{ export function registerWorkadvntureCallback<T extends Function>(callbacks: Array<{
type: keyof IframeResponseEventMap, type: keyof IframeResponseEventMap,
@ -25,6 +25,3 @@ export function registerWorkadvntureCallback<T extends Function>(callbacks: Arra
} }
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
window.parent.postMessage(content, "*")
}

View File

@ -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<number, Popup> = new Map<number, Popup>();
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
interface ZonedPopupOptions {
zone: string
objectLayerName?: string,
popupText: string,
delay?: number
popupOptions: Array<ButtonDescriptor>
}
class PopupApiContribution extends IframeApiContribution<PopupApiContribution> {
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<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++;
}
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()

View File

@ -1,27 +1,54 @@
import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent' 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<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
class WorkadventureZoneCommands extends IframeApiContribution<WorkadventureZoneCommands> {
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 { 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 { 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);
} }
} }
export default new WorkadventureZoneCommands();
export const commands = registerWorkadventureCommand(new WorkadventureZoneCommands())
export const callbacks = registerWorkadvntureCallback([{
callback: (enterEvent: EnterLeaveEvent) => {
},
type: "enterEvent",
typeChecker: isEnterLeaveEvent
},])

View File

@ -9,23 +9,54 @@ import type { ClosePopupEvent } from "./Api/Events/ClosePopupEvent";
import type { OpenTabEvent } from "./Api/Events/OpenTabEvent"; import type { OpenTabEvent } from "./Api/Events/OpenTabEvent";
import type { GoToPageEvent } from "./Api/Events/GoToPageEvent"; import type { GoToPageEvent } from "./Api/Events/GoToPageEvent";
import type { OpenCoWebSiteEvent } from "./Api/Events/OpenCoWebSiteEvent"; 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([ const importType = Promise.all([
import("./Api/iframe/popup"),
import("./Api/iframe/chatmessage"), import("./Api/iframe/chatmessage"),
import("./Api/iframe/zone-events") import("./Api/iframe/zone-events")
]) ])
type UnPromise<P> = P extends Promise<infer T> ? T : P
type WorkadventureCommandClasses = UnPromise<typeof importType>[number]["commands"]; type PromiseReturnType<P> = P extends Promise<infer T> ? T : P
type WorkadventureCommandClasses = PromiseReturnType<typeof importType>[number]["default"];
type KeysOfUnion<T> = T extends T ? keyof T : never type KeysOfUnion<T> = T extends T ? keyof T : never
type ObjectWithKeyOfUnion<O, Key> = O extends O ? (Key extends keyof O ? O[Key] : never) : never
type WorkAdventureApiFiles = { [Key in KeysOfUnion<WorkadventureCommandClasses>]: ObjectWithKeyOfUnion<WorkadventureCommandClasses, Key> }; type ObjectWithKeyOfUnion<Key, O = WorkadventureCommandClasses> = O extends O ? (Key extends keyof O ? O[Key] : never) : never
type ApiKeys = KeysOfUnion<WorkadventureCommandClasses>;
type ObjectOfKey<Key extends ApiKeys, O = WorkadventureCommandClasses> = O extends O ? (Key extends keyof O ? O : never) : never
type ShouldAddAttribute<Key extends ApiKeys> = ObjectWithKeyOfUnion<Key>;
type WorkadventureFunctions = { [K in ApiKeys]: ObjectWithKeyOfUnion<K> extends Function ? K : never }[ApiKeys]
type WorkadventureFunctionsFilteredByRoot = { [K in WorkadventureFunctions]: ObjectOfKey<K>["addMethodsAtRoot"] extends true ? K : never }[WorkadventureFunctions]
type JustMethodKeys<T> = ({ [P in keyof T]: T[P] extends Function ? P : never })[keyof T];
type JustMethods<T> = Pick<T, JustMethodKeys<T>>;
type SubObjectTypes = {
[importCl in WorkadventureCommandClasses as importCl["subObjectIdentifier"]]: JustMethods<importCl>;
};
type WorkAdventureApiFiles = {
[Key in WorkadventureFunctionsFilteredByRoot]: ShouldAddAttribute<Key>
} & SubObjectTypes
export interface WorkAdventureApi extends WorkAdventureApiFiles { 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; openTab(url : string): void;
goToPage(url : string): void; goToPage(url : string): void;
openCoWebSite(url : string): void; openCoWebSite(url : string): void;
@ -47,47 +78,11 @@ declare global {
let WA: WorkAdventureApi let WA: WorkAdventureApi
} }
type ChatMessageCallback = (message: string) => void;
type ButtonClickedCallback = (popup: Popup) => void;
const userInputChatStream: Subject<UserInputChatEvent> = new Subject(); 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 = { window.WA = {
/** /**
@ -152,59 +147,16 @@ window.WA = {
}, '*'); }, '*');
}, },
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup { registerMenuCommand(commandDescriptor: string, callback: (commandDescriptor: string) => void) {
popupId++; menuCallbacks.set(commandDescriptor, callback);
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({ window.parent.postMessage({
'type': 'openPopup', 'type': 'registerMenuCommand',
'data': { 'data': {
popupId, menutItem: commandDescriptor
targetObject, } as MenuItemRegisterEvent
message,
buttons: buttons.map((button) => {
return {
label: button.label,
className: button.className
};
})
} as OpenPopupEvent
}, '*'); }, '*');
popups.set(popupId, popup)
return popup;
}, },
...({} as WorkAdventureApiFiles), ...({} as WorkAdventureApiFiles),
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 => { window.addEventListener('message', message => {
@ -226,22 +178,9 @@ window.addEventListener('message', message => {
if (payload.type === 'userInputChat' && isUserInputChatEvent(payloadData)) { if (payload.type === 'userInputChat' && isUserInputChatEvent(payloadData)) {
userInputChatStream.next(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);
}
} }
} }
// ... // ...
}); });