Implement UI Website (#2087)

* Implement UI Website system

* Add UIWebsite documentation

* Implement review fixes

* Add getAll function on UIWebsite

Co-authored-by: Alexis Faizeau <a.faizeau@workadventu.re>
This commit is contained in:
Alexis Faizeau 2022-05-04 18:30:31 +02:00 committed by GitHub
parent d47aad2ead
commit 9c5fcd2fd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1514 additions and 16 deletions

View File

@ -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
</div>
</div>
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<void>`. 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).
<div class="col">
<img src="images/ui-website.png" class="figure-img img-fluid rounded" alt="" />
</div>
This functonnality creates an iframe positionned on the viewport.
## Display a UI website
```ts
WA.ui.website.open(website: CreateUIWebsiteEvent): Promise<UIWebsite>
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<void>, // Close the current website instance
}
```
You can open a website with the `WA.ui.website.open()` method. It returns an `Promise<UIWebsite>` 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<UIWebsite[]>` instance.
```ts
WA.ui.website.getAll();
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

1
front/.gitignore vendored
View File

@ -5,3 +5,4 @@ dist/
*.sh
!templater.sh
/public/iframe_api.js
/public/es.js

View File

@ -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"
]
}
}
}

View File

@ -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<T> 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<typeof isIframeEventWrapper>;
@ -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;

View File

@ -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<typeof isUIWebsiteCSSValue>;
export const isUIWebsiteMargin = z.object({
top: isUIWebsiteCSSValue.optional(),
bottom: isUIWebsiteCSSValue.optional(),
left: isUIWebsiteCSSValue.optional(),
right: isUIWebsiteCSSValue.optional(),
});
export type UIWebsiteMargin = z.infer<typeof isUIWebsiteMargin>;
export const isViewportPositionVertical = z.enum(["top", "middle", "bottom"]);
export type ViewportPositionVertical = z.infer<typeof isViewportPositionVertical>;
export const isViewportPositionHorizontal = z.enum(["left", "middle", "right"]);
export type ViewportPositionHorizontal = z.infer<typeof isViewportPositionHorizontal>;
export const isUIWebsitePosition = z.object({
vertical: isViewportPositionVertical,
horizontal: isViewportPositionHorizontal,
});
export type UIWebsitePosition = z.infer<typeof isUIWebsitePosition>;
export const isUIWebsiteSize = z.object({
height: isUIWebsiteCSSValue,
width: isUIWebsiteCSSValue,
});
export type UIWebsiteSize = z.infer<typeof isUIWebsiteSize>;
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<typeof isCreateUIWebsiteEvent>;
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<typeof isModifyUIWebsiteEvent>;
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<typeof isUIWebsite>;

View File

@ -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<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"],
@ -112,6 +113,9 @@ class IframeListener {
private readonly _modifyEmbeddedWebsiteStream: Subject<ModifyEmbeddedWebsiteEvent> = new Subject();
public readonly modifyEmbeddedWebsiteStream = this._modifyEmbeddedWebsiteStream.asObservable();
private readonly _modifyUIWebsiteStream: Subject<ModifyUIWebsiteEvent> = new Subject();
public readonly modifyUIWebsiteStream = this._modifyUIWebsiteStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>();
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>();
@ -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(() => {

View File

@ -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<UIWebsiteCommands> {
callbacks = [];
async open(createUIWebsite: CreateUIWebsiteEvent): Promise<UIWebsite> {
const result = await queryWorkadventure({
type: "openUIWebsite",
data: createUIWebsite,
});
return new UIWebsite(result);
}
async getAll(): Promise<UIWebsite[]> {
const result = await queryWorkadventure({
type: "getUIWebsites",
data: undefined,
});
return result.map((current) => new UIWebsite(current));
}
}
export default new UIWebsiteCommands();

View File

@ -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<number, Popup> = new Map<number, Popup>();
@ -283,6 +285,10 @@ export class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventure
actionMessages.set(actionMessage.uuid, actionMessage);
return actionMessage;
}
get website(): UIWebsiteCommands {
return website;
}
}
export default new WorkAdventureUiCommands();

View File

@ -39,6 +39,8 @@
import ActionsMenu from "./ActionsMenu/ActionsMenu.svelte";
import Lazy from "./Lazy.svelte";
import { showDesktopCapturerSourcePicker } from "../Stores/ScreenSharingStore";
import UiWebsiteContainer from "./UI/Website/UIWebsiteContainer.svelte";
import { uiWebsitesStore } from "../Stores/UIWebsiteStore";
let mainLayout: HTMLDivElement;
@ -128,6 +130,10 @@
{#if hasEmbedScreen}
<EmbedScreensContainer />
{/if}
{#if $uiWebsitesStore}
<UiWebsiteContainer />
{/if}
</section>
<section id="main-layout-baseline">

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { uiWebsitesStore } from "../../../Stores/UIWebsiteStore";
import UiWebsiteLayer from "./UIWebsiteLayer.svelte";
</script>
<div id="ui-website-container">
{#each $uiWebsitesStore.reverse() as uiWebsite (uiWebsite.id)}
<UiWebsiteLayer {uiWebsite} />
{/each}
</div>
<style lang="scss">
#ui-website-container {
position: absolute;
z-index: 180;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
</style>

View File

@ -0,0 +1,66 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import { UIWebsite } from "../../../Api/Events/ui/UIWebsite";
import { iframeListener } from "../../../Api/IframeListener";
export let uiWebsite: UIWebsite;
let main: HTMLDivElement;
const iframe = document.createElement("iframe");
$: {
iframe.id = `ui-website-${uiWebsite.id}`;
iframe.src = uiWebsite.url;
iframe.title = uiWebsite.url;
iframe.style.border = "0";
iframe.allow = uiWebsite.allowPolicy ?? "";
iframe.style.height = uiWebsite.size.height;
iframe.style.width = uiWebsite.size.width;
iframe.style.visibility = uiWebsite.visible ? "visible" : "hidden";
iframe.style.margin = uiWebsite.margin
? `${uiWebsite.margin.top ? uiWebsite.margin.top : "O"} ${
uiWebsite.margin.right ? uiWebsite.margin.right : "O"
} ${uiWebsite.margin.bottom ? uiWebsite.margin.bottom : "O"} ${
uiWebsite.margin.left ? uiWebsite.margin.left : "O"
}`
: "0";
}
onMount(() => {
main.appendChild(iframe);
if (uiWebsite.allowApi) {
iframeListener.registerIframe(iframe);
}
});
onDestroy(() => {
if (uiWebsite.allowApi) {
iframeListener.unregisterIframe(iframe);
}
});
</script>
<div
bind:this={main}
class="layer"
style:justify-content={uiWebsite.position.horizontal === "middle"
? "center"
: uiWebsite.position.horizontal === "right"
? "end"
: "start"}
style:align-items={uiWebsite.position.vertical === "middle"
? "center"
: uiWebsite.position.vertical === "bottom"
? "end"
: "top"}
/>
<style lang="scss">
.layer {
height: 100%;
width: 100%;
display: flex;
position: absolute;
}
</style>

View File

@ -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();

View File

@ -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();

View File

@ -0,0 +1,20 @@
import { writable } from "svelte/store";
import { UIWebsite } from "../Api/Events/ui/UIWebsite";
function createUIWebsiteStore() {
const { subscribe, update, set } = writable(Array<UIWebsite>());
set(Array<UIWebsite>());
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();

View File

@ -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"

View File

@ -0,0 +1,5 @@
<html>
<body style="background-color: white;">
This is test page
</body>
</html>

View File

@ -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();
}
});
}

View File

@ -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
}

View File

@ -363,6 +363,14 @@
<a href="#" class="testLink" data-testmap="Modules/without_modules.json" target="_blank">Testing scripts with modules mode disabled</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-uiwebsite"> Success <input type="radio" name="test-uiwebsite"> Failure <input type="radio" name="test-uiwebsite" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="UIWebsite/uiwebsite.json" target="_blank">Testing UIWebsites</a>
</td>
</tr>
</table>
<h2>CoWebsite</h2>
<table class="table">