diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index 2fb97e0a..db460ad0 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -34,7 +34,7 @@ Please note that `openPopup` returns an object of the `Popup` class. Also, the c The `Popup` class that represents an open popup contains a single method: `close()`. This will obviously close the popup when called. -```javascript +```ts class Popup { /** * Closes the popup @@ -45,7 +45,7 @@ class Popup { Example: -```javascript +```ts let helloWorldPopup; // Open the popup when we enter a given zone @@ -90,8 +90,8 @@ Custom menu exist only until the map is unloaded, or you leave the iframe zone o -Example: -```javascript +Example: +```ts const menu = WA.ui.registerMenuCommand('menu test', { callback: () => { @@ -107,7 +107,7 @@ Please note that `registerMenuCommand` returns an object of the `Menu` class. The `Menu` class contains a single method: `remove(): void`. This will obviously remove the menu when called. -```javascript +```ts class Menu { /** * Remove the menu @@ -120,7 +120,7 @@ class Menu { ### Awaiting User Confirmation (with space bar) -``` +```ts WA.ui.displayActionMessage({ message: string, callback: () => void, @@ -136,7 +136,7 @@ Displays a message at the bottom of the screen (that will disappear when space b Example: -```javascript +```ts const triggerMessage = WA.ui.displayActionMessage({ message: "press 'space' to confirm", callback: () => { @@ -154,7 +154,7 @@ Please note that `displayActionMessage` returns an object of the `ActionMessage` The `ActionMessage` class contains a single method: `remove(): Promise`. This will obviously remove the message when called. -```javascript +```ts class ActionMessage { /** * Hides the message @@ -173,7 +173,7 @@ When clicking on other player's WOKA, the contextual menu (we call it ActionsMen To do that, we need to listen for the `onRemotePlayerClicked` stream and make use of the `remotePlayer` object that is passed by as a payload. -```javascript +```ts WA.ui.onRemotePlayerClicked.subscribe((remotePlayer) => { remotePlayer.addAction('Ask to tell a joke', () => { console.log('I am NOT telling you a joke!'); @@ -182,7 +182,7 @@ WA.ui.onRemotePlayerClicked.subscribe((remotePlayer) => { ``` `remotePlayer.addAction(actionName, callback)` returns an Action object, which can remove itself from ActionsMenu: -```javascript +```ts const action = remotePlayer.addAction('This will disappear!', () => { console.log('You managed to click me!'); }); @@ -193,3 +193,95 @@ setTimeout( 1000, ); ``` + +# Open fixed iframes + +You can use the scripting API to display an iframe (so any HTML element) above the game. The iframe is positionned relative to the browser window (so unlike [embedded websites](website-in-map.md), the position of the iframe does not move when someone walks on the map). + +
+ +
+ +This functonnality creates an iframe positionned on the viewport. + +## Display a UI website + +```ts +WA.ui.website.open(website: CreateUIWebsiteEvent): Promise + +interface CreateUIWebsiteEvent { + url: string, // Website URL + visible?: boolean, // The website is visible or not + allowApi?: boolean, // Allow scripting API on the website + allowPolicy?: string, // The list of feature policies allowed + position: { + vertical: "top"|"middle"|"bottom",, + horizontal: "left","middle","right", + }, + size: { // Size on the UI (available units: px|em|%|cm|in|pc|pt|mm|ex|vw|vh|rem and others values auto|inherit) + height: string, + width: string, + }, + margin?: { // Website margin (available units: px|em|%|cm|in|pc|pt|mm|ex|vw|vh|rem and others values auto|inherit) + top?: string, + bottom?: string, + left?: string, + right?: string, + }, +} + +interface UIWebsite { + readonly id: string, // Unique ID + url: string, // Website URL + visible: boolean, // The website is visible or not + readonly allowApi: boolean, // Allow scripting API on the website + readonly allowPolicy: string, // The list of feature policies allowed + position: { + vertical: string, // Vertical position (top, middle, bottom) + horizontal: string, // Horizontal position (left, middle, right) + }, + size: { // Size on the UI (available units: px|em|%|cm|in|pc|pt|mm|ex|vw|vh|rem and others values auto|inherit) + height: string, + width: string, + }, + margin?: { // Website margin (available units: px|em|%|cm|in|pc|pt|mm|ex|vw|vh|rem and others values auto|inherit) + top?: string, + bottom?: string, + left?: string, + right?: string, + }, + close(): Promise, // Close the current website instance +} +``` + +You can open a website with the `WA.ui.website.open()` method. It returns an `Promise` instance. + +```ts +const myWebsite = await WA.ui.website.open({ + url: "https://wikipedia.org", + position: { + vertical: "middle", + horizontal: "middle", + }, + size: { + height: "50vh", + width: "50vw", + }, +}); + +myWebsite.position.vertical = "top"; +``` + +## Close a UI website +You can close a website with the close function on the `UIWebsite` object + +```ts +myWebsite.close(); +``` + +## Get all UI websites +You can get all websites with the `WA.ui.website.getAll()` method. It returns an `Promise` instance. + +```ts +WA.ui.website.getAll(); +``` diff --git a/docs/maps/images/ui-website.png b/docs/maps/images/ui-website.png new file mode 100644 index 00000000..67c76a14 Binary files /dev/null and b/docs/maps/images/ui-website.png differ diff --git a/front/.gitignore b/front/.gitignore index b7cc8c89..d411e833 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -5,3 +5,4 @@ dist/ *.sh !templater.sh /public/iframe_api.js +/public/es.js diff --git a/front/package.json b/front/package.json index e6b94010..bb1a2ce3 100644 --- a/front/package.json +++ b/front/package.json @@ -21,7 +21,7 @@ "lint-staged": "^12.3.7", "npm-run-all": "^4.1.5", "prettier": "^2.3.1", - "prettier-plugin-svelte": "^2.5.0", + "prettier-plugin-svelte": "^2.7.0", "sass": "^1.49.7", "svelte": "^3.46.3", "svelte-check": "^2.1.0", @@ -95,4 +95,4 @@ "yarn run pretty" ] } -} +} \ No newline at end of file diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index f584f6d9..ce8e3cce 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -39,6 +39,7 @@ import type { RemotePlayerClickedEvent } from "./RemotePlayerClickedEvent"; import { isAddActionsMenuKeyToRemotePlayerEvent } from "./AddActionsMenuKeyToRemotePlayerEvent"; import type { ActionsMenuActionClickedEvent } from "./ActionsMenuActionClickedEvent"; import { isRemoveActionsMenuKeyFromRemotePlayerEvent } from "./RemoveActionsMenuKeyFromRemotePlayerEvent"; +import { isCreateUIWebsiteEvent, isModifyUIWebsiteEvent, isUIWebsite } from "./ui/UIWebsite"; export interface TypedMessageEvent extends MessageEvent { data: T; @@ -152,6 +153,10 @@ export const isIframeEventWrapper = z.union([ type: z.literal("modifyEmbeddedWebsite"), data: isEmbeddedWebsiteEvent, }), + z.object({ + type: z.literal("modifyUIWebsite"), + data: isModifyUIWebsiteEvent, + }), ]); export type IframeEvent = z.infer; @@ -261,6 +266,18 @@ export const iframeQueryMapTypeGuards = { query: isMovePlayerToEventConfig, answer: isMovePlayerToEventAnswer, }, + openUIWebsite: { + query: isCreateUIWebsiteEvent, + answer: isUIWebsite, + }, + closeUIWebsite: { + query: z.string(), + answer: z.undefined(), + }, + getUIWebsites: { + query: z.undefined(), + answer: z.array(isUIWebsite), + }, }; type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards; diff --git a/front/src/Api/Events/ui/UIWebsite.ts b/front/src/Api/Events/ui/UIWebsite.ts new file mode 100644 index 00000000..37ffb605 --- /dev/null +++ b/front/src/Api/Events/ui/UIWebsite.ts @@ -0,0 +1,75 @@ +import { z } from "zod"; + +const regexUnit = /-*\d+(px|em|%|cm|in|pc|pt|mm|ex|vw|vh|rem)|auto|inherit/; + +// Parse the string to check if is a valid CSS unit (px,%,vw,vh...) +export const isUIWebsiteCSSValue = z.string().regex(regexUnit); + +export type UIWebsiteCSSValue = z.infer; + +export const isUIWebsiteMargin = z.object({ + top: isUIWebsiteCSSValue.optional(), + bottom: isUIWebsiteCSSValue.optional(), + left: isUIWebsiteCSSValue.optional(), + right: isUIWebsiteCSSValue.optional(), +}); + +export type UIWebsiteMargin = z.infer; + +export const isViewportPositionVertical = z.enum(["top", "middle", "bottom"]); + +export type ViewportPositionVertical = z.infer; + +export const isViewportPositionHorizontal = z.enum(["left", "middle", "right"]); + +export type ViewportPositionHorizontal = z.infer; + +export const isUIWebsitePosition = z.object({ + vertical: isViewportPositionVertical, + horizontal: isViewportPositionHorizontal, +}); + +export type UIWebsitePosition = z.infer; + +export const isUIWebsiteSize = z.object({ + height: isUIWebsiteCSSValue, + width: isUIWebsiteCSSValue, +}); + +export type UIWebsiteSize = z.infer; + +export const isCreateUIWebsiteEvent = z.object({ + url: z.string(), + visible: z.optional(z.boolean()), + allowApi: z.optional(z.boolean()), + allowPolicy: z.optional(z.string()), + position: isUIWebsitePosition, + size: isUIWebsiteSize, + margin: isUIWebsiteMargin.optional(), +}); + +export type CreateUIWebsiteEvent = z.infer; + +export const isModifyUIWebsiteEvent = z.object({ + id: z.string(), + url: z.string().optional(), + visible: z.boolean().optional(), + position: isUIWebsitePosition.optional(), + size: isUIWebsiteSize.optional(), + margin: isUIWebsiteMargin.optional(), +}); + +export type ModifyUIWebsiteEvent = z.infer; + +export const isUIWebsite = z.object({ + id: z.string(), + url: z.string(), + visible: z.boolean(), + allowApi: z.boolean(), + allowPolicy: z.string(), + position: isUIWebsitePosition, + size: isUIWebsiteSize, + margin: isUIWebsiteMargin.optional(), +}); + +export type UIWebsite = z.infer; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 31a89035..5796bfc7 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -35,6 +35,7 @@ import type { RemotePlayerClickedEvent } from "./Events/RemotePlayerClickedEvent import { AddActionsMenuKeyToRemotePlayerEvent } from "./Events/AddActionsMenuKeyToRemotePlayerEvent"; import type { ActionsMenuActionClickedEvent } from "./Events/ActionsMenuActionClickedEvent"; import { RemoveActionsMenuKeyFromRemotePlayerEvent } from "./Events/RemoveActionsMenuKeyFromRemotePlayerEvent"; +import { ModifyUIWebsiteEvent } from "./Events/ui/UIWebsite"; type AnswererCallback = ( query: IframeQueryMap[T]["query"], @@ -112,6 +113,9 @@ class IframeListener { private readonly _modifyEmbeddedWebsiteStream: Subject = new Subject(); public readonly modifyEmbeddedWebsiteStream = this._modifyEmbeddedWebsiteStream.asObservable(); + private readonly _modifyUIWebsiteStream: Subject = new Subject(); + public readonly modifyUIWebsiteStream = this._modifyUIWebsiteStream.asObservable(); + private readonly iframes = new Set(); private readonly iframeCloseCallbacks = new Map void)[]>(); private readonly scripts = new Map(); @@ -276,6 +280,8 @@ class IframeListener { this._setTilesStream.next(iframeEvent.data); } else if (iframeEvent.type == "modifyEmbeddedWebsite") { this._modifyEmbeddedWebsiteStream.next(iframeEvent.data); + } else if (iframeEvent.type == "modifyUIWebsite") { + this._modifyUIWebsiteStream.next(iframeEvent.data); } else if (iframeEvent.type == "registerMenu") { const dataName = iframeEvent.data.name; this.iframeCloseCallbacks.get(iframe)?.push(() => { diff --git a/front/src/Api/iframe/Ui/UIWebsite.ts b/front/src/Api/iframe/Ui/UIWebsite.ts new file mode 100644 index 00000000..d5d8a1ad --- /dev/null +++ b/front/src/Api/iframe/Ui/UIWebsite.ts @@ -0,0 +1,338 @@ +import { + CreateUIWebsiteEvent, + UIWebsiteCSSValue, + UIWebsiteMargin, + UIWebsitePosition, + UIWebsiteSize, + ViewportPositionHorizontal, + ViewportPositionVertical, + UIWebsite as UIWebsiteCore, +} from "../../Events/ui/UIWebsite"; +import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "../IframeApiContribution"; + +class UIWebsitePositionInternal { + private readonly website: UIWebsite; + private _vertical: ViewportPositionVertical; + private _horizontal: ViewportPositionHorizontal; + + constructor(uiWebsite: UIWebsite, position: UIWebsitePosition) { + this.website = uiWebsite; + this._vertical = position.vertical; + this._horizontal = position.horizontal; + } + + public get vertical() { + return this._vertical; + } + + public set vertical(vertical: ViewportPositionVertical) { + this._vertical = vertical; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + position: { + vertical: this._vertical, + horizontal: this._horizontal, + }, + }, + }); + } + + public get horizontal() { + return this._horizontal; + } + + public set horizontal(horizontal: ViewportPositionHorizontal) { + this._horizontal = horizontal; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + position: { + vertical: this._vertical, + horizontal: this._horizontal, + }, + }, + }); + } +} + +class UIWebsiteSizeInternal { + private readonly website: UIWebsite; + private _height: UIWebsiteCSSValue; + private _width: UIWebsiteCSSValue; + + constructor(uiWebsite: UIWebsite, size: UIWebsiteSize) { + this.website = uiWebsite; + this._height = size.height; + this._width = size.width; + } + + public get height() { + return this._height; + } + + public set height(height: UIWebsiteCSSValue) { + this._height = height; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + size: { + height: this._height, + width: this._width, + }, + }, + }); + } + + public get width() { + return this._height; + } + + public set width(width: UIWebsiteCSSValue) { + this._width = width; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + size: { + height: this._height, + width: this._width, + }, + }, + }); + } +} + +class UIWebsiteMarginInternal { + private readonly website: UIWebsite; + private _top?: UIWebsiteCSSValue; + private _bottom?: UIWebsiteCSSValue; + private _left?: UIWebsiteCSSValue; + private _right?: UIWebsiteCSSValue; + + constructor(uiWebsite: UIWebsite, margin: UIWebsiteMargin) { + this.website = uiWebsite; + this._top = margin.top; + this._bottom = margin.bottom; + this._left = margin.left; + this._right = margin.right; + } + + public get top() { + return this._top; + } + + public set top(top: UIWebsiteCSSValue | undefined) { + this._top = top; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + margin: { + top: this._top, + }, + }, + }); + } + + public get bottom() { + return this._bottom; + } + + public set bottom(bottom: UIWebsiteCSSValue | undefined) { + this._bottom = bottom; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + margin: { + bottom: this._bottom, + }, + }, + }); + } + + public get left() { + return this._left; + } + + public set left(left: UIWebsiteCSSValue | undefined) { + this._left = left; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + margin: { + left: this._left, + }, + }, + }); + } + + public get right() { + return this._right; + } + + public set right(right: UIWebsiteCSSValue | undefined) { + this._right = right; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + margin: { + right: this._right, + }, + }, + }); + } +} + +export class UIWebsite { + public readonly id: string; + private _url: string; + private _visible: boolean; + private readonly _allowPolicy: string; + private readonly _allowApi: boolean; + private _position: UIWebsitePositionInternal; + private _size: UIWebsiteSizeInternal; + private _margin: UIWebsiteMarginInternal; + + constructor(config: UIWebsiteCore) { + this.id = config.id; + this._url = config.url; + this._visible = config.visible ?? true; + this._allowPolicy = config.allowPolicy ?? ""; + this._allowApi = config.allowApi ?? false; + this._position = new UIWebsitePositionInternal(this, config.position); + this._size = new UIWebsiteSizeInternal(this, config.size); + this._margin = new UIWebsiteMarginInternal(this, config.margin ?? {}); + } + + public get url() { + return this._url; + } + + public set url(url: string) { + this._url = url; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.id, + url: this._url, + }, + }); + } + + public get visible() { + return this._visible; + } + + public set visible(visible: boolean) { + this._visible = visible; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.id, + visible: this._visible, + }, + }); + } + + public get allowPolicy() { + return this._allowPolicy; + } + + public get allowApi() { + return this._allowApi; + } + + public get position() { + return this._position; + } + + public set position(position: UIWebsitePosition) { + this._position = new UIWebsitePositionInternal(this, position); + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.id, + position: { + vertical: this._position.vertical, + horizontal: this._position.horizontal, + }, + }, + }); + } + + public get size() { + return this._size; + } + + public set size(size: UIWebsiteSize) { + this._size = new UIWebsiteSizeInternal(this, size); + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.id, + size: { + height: this._size.height, + width: this._size.width, + }, + }, + }); + } + + public get margin() { + return this._margin; + } + + public set margin(margin: UIWebsiteMargin) { + this._margin = new UIWebsiteMarginInternal(this, margin); + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.id, + margin: { + top: this._margin.top, + bottom: this._margin.bottom, + left: this._margin.left, + right: this._margin.right, + }, + }, + }); + } + + close() { + return queryWorkadventure({ + type: "closeUIWebsite", + data: this.id, + }); + } +} + +export class UIWebsiteCommands extends IframeApiContribution { + callbacks = []; + + async open(createUIWebsite: CreateUIWebsiteEvent): Promise { + const result = await queryWorkadventure({ + type: "openUIWebsite", + data: createUIWebsite, + }); + + return new UIWebsite(result); + } + + async getAll(): Promise { + const result = await queryWorkadventure({ + type: "getUIWebsites", + data: undefined, + }); + + return result.map((current) => new UIWebsite(current)); + } +} + +export default new UIWebsiteCommands(); diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index 15f0756c..b9edcff7 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -14,6 +14,8 @@ import { isActionsMenuActionClickedEvent, } from "../Events/ActionsMenuActionClickedEvent"; import { Observable, Subject } from "rxjs"; +import type { UIWebsiteCommands } from "./Ui/UIWebsite"; +import website from "./Ui/UIWebsite"; let popupId = 0; const popups: Map = new Map(); @@ -283,6 +285,10 @@ export class WorkAdventureUiCommands extends IframeApiContribution {/if} + + {#if $uiWebsitesStore} + + {/if}
diff --git a/front/src/Components/UI/Website/UIWebsiteContainer.svelte b/front/src/Components/UI/Website/UIWebsiteContainer.svelte new file mode 100644 index 00000000..7e6d9ebb --- /dev/null +++ b/front/src/Components/UI/Website/UIWebsiteContainer.svelte @@ -0,0 +1,21 @@ + + +
+ {#each $uiWebsitesStore.reverse() as uiWebsite (uiWebsite.id)} + + {/each} +
+ + diff --git a/front/src/Components/UI/Website/UIWebsiteLayer.svelte b/front/src/Components/UI/Website/UIWebsiteLayer.svelte new file mode 100644 index 00000000..229e653b --- /dev/null +++ b/front/src/Components/UI/Website/UIWebsiteLayer.svelte @@ -0,0 +1,66 @@ + + +
+ + diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5100b90c..c573cea7 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -102,6 +102,7 @@ import CancelablePromise from "cancelable-promise"; import { Deferred } from "ts-deferred"; import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin"; import { ErrorScreenMessage, PlayerDetailsUpdatedMessage } from "../../Messages/ts-proto-generated/protos/messages"; +import { uiWebsiteManager } from "./UI/UIWebsiteManager"; export interface GameSceneInitInterface { initPosition: PointInterface | null; reconnecting: boolean; @@ -1298,6 +1299,18 @@ ${escapedMessage} return coWebsiteManager.closeCoWebsites(); }); + iframeListener.registerAnswerer("openUIWebsite", (websiteConfig) => { + return uiWebsiteManager.open(websiteConfig); + }); + + iframeListener.registerAnswerer("getUIWebsites", () => { + return uiWebsiteManager.getAll(); + }); + + iframeListener.registerAnswerer("closeUIWebsite", (websiteId) => { + return uiWebsiteManager.close(websiteId); + }); + iframeListener.registerAnswerer("getMapData", () => { return { data: this.gameMap.getMap(), @@ -1594,6 +1607,9 @@ ${escapedMessage} iframeListener.unregisterAnswerer("getCoWebsites"); iframeListener.unregisterAnswerer("setPlayerOutline"); iframeListener.unregisterAnswerer("setVariable"); + iframeListener.unregisterAnswerer("openUIWebsite"); + iframeListener.unregisterAnswerer("getUIWebsites"); + iframeListener.unregisterAnswerer("closeUIWebsite"); this.sharedVariablesManager?.close(); this.embeddedWebsiteManager?.close(); diff --git a/front/src/Phaser/Game/UI/UIWebsiteManager.ts b/front/src/Phaser/Game/UI/UIWebsiteManager.ts new file mode 100644 index 00000000..ce3bb0d6 --- /dev/null +++ b/front/src/Phaser/Game/UI/UIWebsiteManager.ts @@ -0,0 +1,94 @@ +import { get } from "svelte/store"; +import { CreateUIWebsiteEvent, ModifyUIWebsiteEvent, UIWebsite } from "../../../Api/Events/ui/UIWebsite"; +import { iframeListener } from "../../../Api/IframeListener"; +import { v4 as uuidv4 } from "uuid"; +import { uiWebsitesStore } from "../../../Stores/UIWebsiteStore"; + +class UIWebsiteManager { + constructor() { + iframeListener.modifyUIWebsiteStream.subscribe((websiteEvent: ModifyUIWebsiteEvent) => { + const website = get(uiWebsitesStore).find((currentWebsite) => currentWebsite.id === websiteEvent.id); + if (!website) { + throw new Error(`Could not find ui website with the id "${websiteEvent.id}" in your map`); + } + + if (websiteEvent.url) { + website.url = websiteEvent.url; + } + + if (websiteEvent.visible !== undefined) { + website.visible = websiteEvent.visible; + } + + if (websiteEvent.position) { + if (websiteEvent.position.horizontal) { + website.position.horizontal = websiteEvent.position.horizontal; + } + + if (websiteEvent.position.vertical) { + website.position.vertical = websiteEvent.position.vertical; + } + } + + if (websiteEvent.size) { + if (websiteEvent.size.height) { + website.size.height = websiteEvent.size.height; + } + + if (websiteEvent.size.width) { + website.size.width = websiteEvent.size.width; + } + } + + if (websiteEvent.margin) { + website.margin = {}; + + if (websiteEvent.margin.top !== undefined) { + website.margin.top = websiteEvent.margin.top; + } + + if (websiteEvent.margin.bottom !== undefined) { + website.margin.bottom = websiteEvent.margin.bottom; + } + + if (websiteEvent.margin.left !== undefined) { + website.margin.left = websiteEvent.margin.left; + } + + if (websiteEvent.margin.right !== undefined) { + website.margin.right = websiteEvent.margin.right; + } + } + }); + } + + public open(websiteConfig: CreateUIWebsiteEvent): UIWebsite { + const newWebsite: UIWebsite = { + ...websiteConfig, + id: uuidv4(), + visible: websiteConfig.visible ?? true, + allowPolicy: websiteConfig.allowPolicy ?? "", + allowApi: websiteConfig.allowApi ?? false, + }; + + uiWebsitesStore.add(newWebsite); + + return newWebsite; + } + + public getAll(): UIWebsite[] { + return get(uiWebsitesStore); + } + + public close(websiteId: string) { + const uiWebsite = get(uiWebsitesStore).find((currentWebsite) => currentWebsite.id === websiteId); + + if (!uiWebsite) { + return; + } + + uiWebsitesStore.remove(uiWebsite); + } +} + +export const uiWebsiteManager = new UIWebsiteManager(); diff --git a/front/src/Stores/UIWebsiteStore.ts b/front/src/Stores/UIWebsiteStore.ts new file mode 100644 index 00000000..37c17156 --- /dev/null +++ b/front/src/Stores/UIWebsiteStore.ts @@ -0,0 +1,20 @@ +import { writable } from "svelte/store"; +import { UIWebsite } from "../Api/Events/ui/UIWebsite"; + +function createUIWebsiteStore() { + const { subscribe, update, set } = writable(Array()); + + set(Array()); + + return { + subscribe, + add: (uiWebsite: UIWebsite) => { + update((currentArray) => [...currentArray, uiWebsite]); + }, + remove: (uiWebsite: UIWebsite) => { + update((currentArray) => currentArray.filter((currentWebsite) => currentWebsite.id !== uiWebsite.id)); + }, + }; +} + +export const uiWebsitesStore = createUIWebsiteStore(); diff --git a/front/yarn.lock b/front/yarn.lock index 4729cb7a..7f49a4ed 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2239,10 +2239,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier-plugin-svelte@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.5.0.tgz#7922534729f7febe59b4c56c3f5360539f0d8ab1" - integrity sha512-+iHY2uGChOngrgKielJUnqo74gIL/EO5oeWm8MftFWjEi213lq9QYTOwm1pv4lI1nA61tdgf80CF2i5zMcu1kw== +prettier-plugin-svelte@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.7.0.tgz#ecfa4fe824238a4466a3497df1a96d15cf43cabb" + integrity sha512-fQhhZICprZot2IqEyoiUYLTRdumULGRvw0o4dzl5jt0jfzVWdGqeYW27QTWAeXhoupEZJULmNoH3ueJwUWFLIA== prettier@^2.0.2: version "2.5.1" diff --git a/maps/tests/UIWebsite/index.html b/maps/tests/UIWebsite/index.html new file mode 100644 index 00000000..6ab2a8e8 --- /dev/null +++ b/maps/tests/UIWebsite/index.html @@ -0,0 +1,5 @@ + + + This is test page + + \ No newline at end of file diff --git a/maps/tests/UIWebsite/script.js b/maps/tests/UIWebsite/script.js new file mode 100644 index 00000000..b58780ba --- /dev/null +++ b/maps/tests/UIWebsite/script.js @@ -0,0 +1,48 @@ +WA.onInit().then(() => { + initListeners(); +}); + +function initListeners() { + let first_website = undefined; + let second_website = undefined; + + WA.room.onEnterLayer('first_website').subscribe(async () => { + first_website = await WA.ui.website.open({ + url: "http://maps.workadventure.localhost/tests/UIWebsite/index.html", + position: { + vertical: "middle", + horizontal: "middle", + }, + size: { + height: "50vh", + width: "50vw", + }, + }); + }); + + WA.room.onLeaveLayer('first_website').subscribe(() => { + if (first_website) { + first_website.close(); + } + }); + + WA.room.onEnterLayer('second_website').subscribe(async () => { + second_website = await WA.ui.website.open({ + url: "https://www.wikipedia.org/", + position: { + vertical: "top", + horizontal: "right", + }, + size: { + height: "20vh", + width: "50vw", + }, + }); + }); + + WA.room.onLeaveLayer('second_website').subscribe(() => { + if (second_website) { + second_website.close(); + } + }); +} diff --git a/maps/tests/UIWebsite/uiwebsite.json b/maps/tests/UIWebsite/uiwebsite.json new file mode 100644 index 00000000..74c1dcc3 --- /dev/null +++ b/maps/tests/UIWebsite/uiwebsite.json @@ -0,0 +1,679 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":10, + "id":1, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23], + "height":10, + "id":5, + "name":"first_website", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, + 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, + 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, + 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, + 0, 0, 0, 0, 0, 12, 12, 12, 12, 12], + "height":10, + "id":7, + "name":"second_website", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[ + { + "height":116.924156284309, + "id":1, + "name":"Tests", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":8, + "text":"Test 1:\nMove on the white carpet to display a UIWebsite.\n\nTest 2:\nMove on the blue carpet to display an other UIWebsite above the first.", + "wrap":true + }, + "type":"", + "visible":true, + "width":158.381128664136, + "x":1.64026713939023, + "y":201.037039933902 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 82, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 19, 27, 0, + 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":8, + "name":"objects", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }], + "nextlayerid":9, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "renderorder":"right-down", + "tiledversion":"1.8.4", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tiles":[ + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":5, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":6, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":7, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":21, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":23, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":24, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":25, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + + { + "id":26, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":27, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":28, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":29, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":30, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":31, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":32, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":34, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":35, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":42, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + + { + "id":43, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":45, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":46, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":59, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":60, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":70, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":71, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":80, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":81, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":89, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + + { + "id":91, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":93, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":94, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":95, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":96, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":97, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":100, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":102, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":103, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":104, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + + { + "id":105, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":106, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":107, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":108, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":114, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":115, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":"1.8", + "width":10 +} \ No newline at end of file diff --git a/maps/tests/index.html b/maps/tests/index.html index 610337ee..68c03f79 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -363,6 +363,14 @@ Testing scripts with modules mode disabled + + + Success Failure Pending + + + Testing UIWebsites + +

CoWebsite