Merge pull request #1312 from thecodingmachine/iframe_in_map

Adding the ability to inject websites directly inside maps
This commit is contained in:
David Négrier 2021-08-05 12:44:23 +02:00 committed by GitHub
commit 56cf535551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 966 additions and 9 deletions

View File

@ -176,4 +176,102 @@ You can create a tileset file in Tile Editor.
WA.room.loadTileset("Assets/Tileset.json").then((firstId) => { WA.room.loadTileset("Assets/Tileset.json").then((firstId) => {
WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]); WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]);
}) })
``` ```
## Embedding websites in a map
You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using the ["website" objects](website-in-map.md)).
### Getting an instance of a website already embedded in the map
```
WA.room.website.get(objectName: string): Promise<EmbeddedWebsite>
```
You can get an instance of an embedded website by using the `WA.room.website.get()` method.
It returns a promise of an `EmbeddedWebsite` instance.
```javascript
// Get an existing website object where 'my_website' is the name of the object (on any layer object of the map)
const website = await WA.room.website.get('my_website');
website.url = 'https://example.com';
website.visible = true;
```
### Adding a new website in a map
```
WA.room.website.create(website: CreateEmbeddedWebsiteEvent): EmbeddedWebsite
interface CreateEmbeddedWebsiteEvent {
name: string; // A unique name for this iframe
url: string; // The URL the iframe points to.
position: {
x: number, // In pixels, relative to the map coordinates
y: number, // In pixels, relative to the map coordinates
width: number, // In pixels, sensitive to zoom level
height: number, // In pixels, sensitive to zoom level
},
visible?: boolean, // Whether to display the iframe or not
allowApi?: boolean, // Whether the scripting API should be available to the iframe
allow?: string, // The list of feature policies allowed
}
```
You can create an instance of an embedded website by using the `WA.room.website.create()` method.
It returns an `EmbeddedWebsite` instance.
```javascript
// Create a new website object
const website = WA.room.website.create({
name: "my_website",
url: "https://example.com",
position: {
x: 64,
y: 128,
width: 320,
height: 240,
},
visible: true,
allowApi: true,
allow: "fullscreen",
});
```
### Deleting a website from a map
```
WA.room.website.delete(name: string): Promise<void>
```
Use `WA.room.website.delete` to completely remove an embedded website from your map.
### The EmbeddedWebsite class
Instances of the `EmbeddedWebsite` class represent the website displayed on the map.
```typescript
class EmbeddedWebsite {
readonly name: string;
url: string;
visible: boolean;
allow: string;
allowApi: boolean;
x: number; // In pixels, relative to the map coordinates
y: number; // In pixels, relative to the map coordinates
width: number; // In pixels, sensitive to zoom level
height: number; // In pixels, sensitive to zoom level
}
```
When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map.
{.alert.alert-warning}
The websites you add/edit/delete via the scripting API are only shown locally. If you want them
to be displayed for every player, you can use [variables](api-start.md) to share a common state
between all users.

View File

@ -0,0 +1,40 @@
{.section-title.accent.text-primary}
# Putting a website inside a map
You can inject a website directly into your map, at a given position.
To do this in Tiled:
- Select an object layer
- Create a rectangular object, at the position where you want your website to appear
- Add a `url` property to your object pointing to the URL you want to open
<div>
<figure class="figure">
<img src="https://workadventu.re/img/docs/website_url_property.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
<figcaption class="figure-caption">A "website" object</figcaption>
</figure>
</div>
The `url` can be absolute, or relative to your map.
{.alert.alert-info}
Internally, WorkAdventure will create an "iFrame" to load the website.
Some websites forbid being opened by iframes using the [`X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options)
HTTP header.
{.alert.alert-warning}
Please note that the website always appears **on top** of the tiles (even if you put the object layer that
contains the "website" object under the tiles).
## Allowing the scripting API in your iframe
If you are planning to use the WorkAdventure scripting API inside your iframe, you need
to explicitly allow it, by setting an additional `allowApi` property to `true`.
<div>
<figure class="figure">
<img src="https://workadventu.re/img/docs/website_allowapi_property.png" class="figure-img img-fluid rounded" alt="" style="width: 70%" />
<figcaption class="figure-caption">A "website" object that can communicate using the Iframe API</figcaption>
</figure>
</div>

View File

@ -0,0 +1,48 @@
import * as tg from "generic-type-guard";
export const isRectangle = new tg.IsInterface()
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
width: tg.isNumber,
height: tg.isNumber,
})
.get();
export const isEmbeddedWebsiteEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
})
.withOptionalProperties({
url: tg.isString,
visible: tg.isBoolean,
allowApi: tg.isBoolean,
allow: tg.isString,
x: tg.isNumber,
y: tg.isNumber,
width: tg.isNumber,
height: tg.isNumber,
})
.get();
export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface()
.withProperties({
name: tg.isString,
url: tg.isString,
position: isRectangle,
})
.withOptionalProperties({
visible: tg.isBoolean,
allowApi: tg.isBoolean,
allow: tg.isString,
})
.get();
/**
* A message sent from the iFrame to the game to modify an embedded website
*/
export type ModifyEmbeddedWebsiteEvent = tg.GuardedType<typeof isEmbeddedWebsiteEvent>;
export type CreateEmbeddedWebsiteEvent = tg.GuardedType<typeof isCreateEmbeddedWebsiteEvent>;
// TODO: make a variation that is all optional (except for the name)
export type Rectangle = tg.GuardedType<typeof isRectangle>;

View File

@ -22,6 +22,8 @@ import type { SetVariableEvent } from "./SetVariableEvent";
import { isGameStateEvent } from "./GameStateEvent"; import { isGameStateEvent } from "./GameStateEvent";
import { isMapDataEvent } from "./MapDataEvent"; import { isMapDataEvent } from "./MapDataEvent";
import { isSetVariableEvent } from "./SetVariableEvent"; import { isSetVariableEvent } from "./SetVariableEvent";
import type { EmbeddedWebsite } from "../iframe/Room/EmbeddedWebsite";
import { isCreateEmbeddedWebsiteEvent } from "./EmbeddedWebsiteEvent";
import type { LoadTilesetEvent } from "./LoadTilesetEvent"; import type { LoadTilesetEvent } from "./LoadTilesetEvent";
import { isLoadTilesetEvent } from "./LoadTilesetEvent"; import { isLoadTilesetEvent } from "./LoadTilesetEvent";
import type { import type {
@ -63,6 +65,7 @@ export type IframeEventMap = {
loadTileset: LoadTilesetEvent; loadTileset: LoadTilesetEvent;
registerMenuCommand: MenuItemRegisterEvent; registerMenuCommand: MenuItemRegisterEvent;
setTiles: SetTilesEvent; setTiles: SetTilesEvent;
modifyEmbeddedWebsite: Partial<EmbeddedWebsite>; // Note: name should be compulsory in fact
}; };
export interface IframeEvent<T extends keyof IframeEventMap> { export interface IframeEvent<T extends keyof IframeEventMap> {
type: T; type: T;
@ -122,6 +125,18 @@ export const iframeQueryMapTypeGuards = {
query: isMessageReferenceEvent, query: isMessageReferenceEvent,
answer: tg.isUndefined, answer: tg.isUndefined,
}, },
getEmbeddedWebsite: {
query: tg.isString,
answer: isCreateEmbeddedWebsiteEvent,
},
deleteEmbeddedWebsite: {
query: tg.isString,
answer: tg.isUndefined,
},
createEmbeddedWebsite: {
query: isCreateEmbeddedWebsiteEvent,
answer: tg.isUndefined,
},
}; };
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never; type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
@ -158,7 +173,12 @@ export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQuer
if (!isIframeQueryKey(type)) { if (!isIframeQueryKey(type)) {
return false; return false;
} }
return iframeQueryMapTypeGuards[type].query(event.data);
const result = iframeQueryMapTypeGuards[type].query(event.data);
if (!result) {
console.warn('Received a query with type "' + type + '" but the payload is invalid.');
}
return result;
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -32,6 +32,8 @@ import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
import type { SetVariableEvent } from "./Events/SetVariableEvent"; import type { SetVariableEvent } from "./Events/SetVariableEvent";
import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
import { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite";
type AnswererCallback<T extends keyof IframeQueryMap> = ( type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"], query: IframeQueryMap[T]["query"],
@ -109,6 +111,9 @@ class IframeListener {
private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject(); private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
public readonly setTilesStream = this._setTilesStream.asObservable(); public readonly setTilesStream = this._setTilesStream.asObservable();
private readonly _modifyEmbeddedWebsiteStream: Subject<ModifyEmbeddedWebsiteEvent> = new Subject();
public readonly modifyEmbeddedWebsiteStream = this._modifyEmbeddedWebsiteStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>(); private readonly iframes = new Set<HTMLIFrameElement>();
private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>(); private readonly iframeCloseCallbacks = new Map<HTMLIFrameElement, (() => void)[]>();
private readonly scripts = new Map<string, HTMLIFrameElement>(); private readonly scripts = new Map<string, HTMLIFrameElement>();
@ -264,6 +269,8 @@ class IframeListener {
handleMenuItemRegistrationEvent(payload.data); handleMenuItemRegistrationEvent(payload.data);
} else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
this._setTilesStream.next(payload.data); this._setTilesStream.next(payload.data);
} else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
this._modifyEmbeddedWebsiteStream.next(payload.data);
} }
} }
}, },

View File

@ -0,0 +1,90 @@
import { sendToWorkadventure } from "../IframeApiContribution";
import type {
CreateEmbeddedWebsiteEvent,
ModifyEmbeddedWebsiteEvent,
Rectangle,
} from "../../Events/EmbeddedWebsiteEvent";
export class EmbeddedWebsite {
public readonly name: string;
private _url: string;
private _visible: boolean;
private _allow: string;
private _allowApi: boolean;
private _position: Rectangle;
constructor(private config: CreateEmbeddedWebsiteEvent) {
this.name = config.name;
this._url = config.url;
this._visible = config.visible ?? true;
this._allow = config.allow ?? "";
this._allowApi = config.allowApi ?? false;
this._position = config.position;
}
public set url(url: string) {
this._url = url;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
url: this._url,
},
});
}
public set visible(visible: boolean) {
this._visible = visible;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
visible: this._visible,
},
});
}
public set x(x: number) {
this._position.x = x;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
x: this._position.x,
},
});
}
public set y(y: number) {
this._position.y = y;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
y: this._position.y,
},
});
}
public set width(width: number) {
this._position.width = width;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
width: this._position.width,
},
});
}
public set height(height: number) {
this._position.height = height;
sendToWorkadventure({
type: "modifyEmbeddedWebsite",
data: {
name: this.name,
height: this._position.height,
},
});
}
}

View File

@ -6,6 +6,8 @@ import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "
import { apiCallback } from "./registeredCallbacks"; import { apiCallback } from "./registeredCallbacks";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
import type { WorkadventureRoomWebsiteCommands } from "./website";
import website from "./website";
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>(); const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>(); const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
@ -105,6 +107,7 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
} }
return mapURL; return mapURL;
} }
async loadTileset(url: string): Promise<number> { async loadTileset(url: string): Promise<number> {
return await queryWorkadventure({ return await queryWorkadventure({
type: "loadTileset", type: "loadTileset",
@ -113,6 +116,10 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
}, },
}); });
} }
get website(): WorkadventureRoomWebsiteCommands {
return website;
}
} }
export default new WorkadventureRoomCommands(); export default new WorkadventureRoomCommands();

View File

@ -0,0 +1,38 @@
import type { LoadSoundEvent } from "../Events/LoadSoundEvent";
import type { PlaySoundEvent } from "../Events/PlaySoundEvent";
import type { StopSoundEvent } from "../Events/StopSoundEvent";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { Sound } from "./Sound/Sound";
import { EmbeddedWebsite } from "./Room/EmbeddedWebsite";
import type { CreateEmbeddedWebsiteEvent } from "../Events/EmbeddedWebsiteEvent";
export class WorkadventureRoomWebsiteCommands extends IframeApiContribution<WorkadventureRoomWebsiteCommands> {
callbacks = [];
async get(objectName: string): Promise<EmbeddedWebsite> {
const websiteEvent = await queryWorkadventure({
type: "getEmbeddedWebsite",
data: objectName,
});
return new EmbeddedWebsite(websiteEvent);
}
create(createEmbeddedWebsiteEvent: CreateEmbeddedWebsiteEvent): EmbeddedWebsite {
queryWorkadventure({
type: "createEmbeddedWebsite",
data: createEmbeddedWebsiteEvent,
}).catch((e) => {
console.error(e);
});
return new EmbeddedWebsite(createEmbeddedWebsiteEvent);
}
async delete(objectName: string): Promise<void> {
return await queryWorkadventure({
type: "deleteEmbeddedWebsite",
data: objectName,
});
}
}
export default new WorkadventureRoomWebsiteCommands();

View File

@ -0,0 +1,198 @@
import type { GameScene } from "./GameScene";
import { iframeListener } from "../../Api/IframeListener";
import type { Subscription } from "rxjs";
import type { CreateEmbeddedWebsiteEvent, ModifyEmbeddedWebsiteEvent } from "../../Api/Events/EmbeddedWebsiteEvent";
import DOMElement = Phaser.GameObjects.DOMElement;
type EmbeddedWebsite = CreateEmbeddedWebsiteEvent & { iframe: HTMLIFrameElement; phaserObject: DOMElement };
export class EmbeddedWebsiteManager {
private readonly embeddedWebsites = new Map<string, EmbeddedWebsite>();
private readonly subscription: Subscription;
constructor(private gameScene: GameScene) {
iframeListener.registerAnswerer("getEmbeddedWebsite", (name: string) => {
const website = this.embeddedWebsites.get(name);
if (website === undefined) {
throw new Error('Cannot find embedded website with name "' + name + '"');
}
const rect = website.iframe.getBoundingClientRect();
return {
url: website.url,
name: website.name,
visible: website.visible,
allowApi: website.allowApi,
allow: website.allow,
position: {
x: website.phaserObject.x,
y: website.phaserObject.y,
width: rect["width"],
height: rect["height"],
},
};
});
iframeListener.registerAnswerer("deleteEmbeddedWebsite", (name: string) => {
const website = this.embeddedWebsites.get(name);
if (!website) {
throw new Error('Could not find website to delete with the name "' + name + '" in your map');
}
website.iframe.remove();
website.phaserObject.destroy();
this.embeddedWebsites.delete(name);
});
iframeListener.registerAnswerer(
"createEmbeddedWebsite",
(createEmbeddedWebsiteEvent: CreateEmbeddedWebsiteEvent) => {
if (this.embeddedWebsites.has(createEmbeddedWebsiteEvent.name)) {
throw new Error('An embedded website with the name "' + name + '" already exists in your map');
}
this.createEmbeddedWebsite(
createEmbeddedWebsiteEvent.name,
createEmbeddedWebsiteEvent.url,
createEmbeddedWebsiteEvent.position.x,
createEmbeddedWebsiteEvent.position.y,
createEmbeddedWebsiteEvent.position.width,
createEmbeddedWebsiteEvent.position.height,
createEmbeddedWebsiteEvent.visible ?? true,
createEmbeddedWebsiteEvent.allowApi ?? false,
createEmbeddedWebsiteEvent.allow ?? ""
);
}
);
this.subscription = iframeListener.modifyEmbeddedWebsiteStream.subscribe(
(embeddedWebsiteEvent: ModifyEmbeddedWebsiteEvent) => {
const website = this.embeddedWebsites.get(embeddedWebsiteEvent.name);
if (!website) {
throw new Error(
'Could not find website with the name "' + embeddedWebsiteEvent.name + '" in your map'
);
}
gameScene.markDirty();
if (embeddedWebsiteEvent.url !== undefined) {
website.url = embeddedWebsiteEvent.url;
const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString();
website.iframe.src = absoluteUrl;
}
if (embeddedWebsiteEvent.visible !== undefined) {
website.visible = embeddedWebsiteEvent.visible;
website.phaserObject.visible = embeddedWebsiteEvent.visible;
}
if (embeddedWebsiteEvent.allowApi !== undefined) {
website.allowApi = embeddedWebsiteEvent.allowApi;
if (embeddedWebsiteEvent.allowApi) {
iframeListener.registerIframe(website.iframe);
} else {
iframeListener.unregisterIframe(website.iframe);
}
}
if (embeddedWebsiteEvent.allow !== undefined) {
website.allow = embeddedWebsiteEvent.allow;
website.iframe.allow = embeddedWebsiteEvent.allow;
}
if (embeddedWebsiteEvent?.x !== undefined) {
website.phaserObject.x = embeddedWebsiteEvent.x;
}
if (embeddedWebsiteEvent?.y !== undefined) {
website.phaserObject.y = embeddedWebsiteEvent.y;
}
if (embeddedWebsiteEvent?.width !== undefined) {
website.iframe.style.width = embeddedWebsiteEvent.width + "px";
}
if (embeddedWebsiteEvent?.height !== undefined) {
website.iframe.style.height = embeddedWebsiteEvent.height + "px";
}
}
);
}
public createEmbeddedWebsite(
name: string,
url: string,
x: number,
y: number,
width: number,
height: number,
visible: boolean,
allowApi: boolean,
allow: string
): void {
if (this.embeddedWebsites.has(name)) {
throw new Error('An embedded website with the name "' + name + '" already exists in your map');
}
const embeddedWebsiteEvent: CreateEmbeddedWebsiteEvent = {
name,
url,
/*x,
y,
width,
height,*/
allow,
allowApi,
visible,
position: {
x,
y,
width,
height,
},
};
const embeddedWebsite = this.doCreateEmbeddedWebsite(embeddedWebsiteEvent, visible);
this.embeddedWebsites.set(name, embeddedWebsite);
}
private doCreateEmbeddedWebsite(
embeddedWebsiteEvent: CreateEmbeddedWebsiteEvent,
visible: boolean
): EmbeddedWebsite {
const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString();
const iframe = document.createElement("iframe");
iframe.src = absoluteUrl;
iframe.style.width = embeddedWebsiteEvent.position.width + "px";
iframe.style.height = embeddedWebsiteEvent.position.height + "px";
iframe.style.margin = "0";
iframe.style.padding = "0";
iframe.style.border = "none";
const embeddedWebsite = {
...embeddedWebsiteEvent,
phaserObject: this.gameScene.add
.dom(embeddedWebsiteEvent.position.x, embeddedWebsiteEvent.position.y, iframe)
.setVisible(visible)
.setOrigin(0, 0),
iframe: iframe,
};
if (embeddedWebsiteEvent.allowApi) {
iframeListener.registerIframe(iframe);
}
return embeddedWebsite;
}
close(): void {
for (const [key, website] of this.embeddedWebsites) {
if (website.allowApi) {
iframeListener.unregisterIframe(website.iframe);
}
}
this.subscription.unsubscribe();
iframeListener.unregisterAnswerer("getEmbeddedWebsite");
iframeListener.unregisterAnswerer("deleteEmbeddedWebsite");
iframeListener.unregisterAnswerer("createEmbeddedWebsite");
}
}

View File

@ -84,10 +84,12 @@ import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStor
import { SharedVariablesManager } from "./SharedVariablesManager"; import { SharedVariablesManager } from "./SharedVariablesManager";
import { playersStore } from "../../Stores/PlayersStore"; import { playersStore } from "../../Stores/PlayersStore";
import { chatVisibilityStore } from "../../Stores/ChatStore"; import { chatVisibilityStore } from "../../Stores/ChatStore";
import { PropertyUtils } from "../Map/PropertyUtils";
import Tileset = Phaser.Tilemaps.Tileset; import Tileset = Phaser.Tilemaps.Tileset;
import { userIsAdminStore } from "../../Stores/GameStore"; import { userIsAdminStore } from "../../Stores/GameStore";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
@ -198,6 +200,8 @@ export class GameScene extends DirtyScene {
private preloading: boolean = true; private preloading: boolean = true;
private startPositionCalculator!: StartPositionCalculator; private startPositionCalculator!: StartPositionCalculator;
private sharedVariablesManager!: SharedVariablesManager; private sharedVariablesManager!: SharedVariablesManager;
private objectsByType = new Map<string, ITiledMapObject[]>();
private embeddedWebsiteManager!: EmbeddedWebsiteManager;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({ super({
@ -337,27 +341,27 @@ export class GameScene extends DirtyScene {
}); });
// Scan the object layers for objects to load and load them. // Scan the object layers for objects to load and load them.
const objects = new Map<string, ITiledMapObject[]>(); this.objectsByType = new Map<string, ITiledMapObject[]>();
for (const layer of this.mapFile.layers) { for (const layer of this.mapFile.layers) {
if (layer.type === "objectgroup") { if (layer.type === "objectgroup") {
for (const object of layer.objects) { for (const object of layer.objects) {
let objectsOfType: ITiledMapObject[] | undefined; let objectsOfType: ITiledMapObject[] | undefined;
if (!objects.has(object.type)) { if (!this.objectsByType.has(object.type)) {
objectsOfType = new Array<ITiledMapObject>(); objectsOfType = new Array<ITiledMapObject>();
} else { } else {
objectsOfType = objects.get(object.type); objectsOfType = this.objectsByType.get(object.type);
if (objectsOfType === undefined) { if (objectsOfType === undefined) {
throw new Error("Unexpected object type not found"); throw new Error("Unexpected object type not found");
} }
} }
objectsOfType.push(object); objectsOfType.push(object);
objects.set(object.type, objectsOfType); this.objectsByType.set(object.type, objectsOfType);
} }
} }
} }
for (const [itemType, objectsOfType] of objects) { for (const [itemType, objectsOfType] of this.objectsByType) {
// FIXME: we would ideally need for the loader to WAIT for the import to be performed, which means writing our own loader plugin. // FIXME: we would ideally need for the loader to WAIT for the import to be performed, which means writing our own loader plugin.
let itemFactory: ItemFactoryInterface; let itemFactory: ItemFactoryInterface;
@ -416,6 +420,7 @@ export class GameScene extends DirtyScene {
//hook create scene //hook create scene
create(): void { create(): void {
console.log("GAAAAAAAGAGAGAGAGA");
this.preloading = false; this.preloading = false;
this.trackDirtyAnims(); this.trackDirtyAnims();
@ -457,6 +462,8 @@ export class GameScene extends DirtyScene {
//permit to set bound collision //permit to set bound collision
this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
this.embeddedWebsiteManager = new EmbeddedWebsiteManager(this);
//add layer on map //add layer on map
this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains); this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains);
for (const layer of this.gameMap.flatLayers) { for (const layer of this.gameMap.flatLayers) {
@ -477,6 +484,28 @@ export class GameScene extends DirtyScene {
if (object.text) { if (object.text) {
TextUtils.createTextFromITiledMapObject(this, object); TextUtils.createTextFromITiledMapObject(this, object);
} }
if (object.type === "website") {
// Let's load iframes in the map
const url = PropertyUtils.mustFindStringProperty(
"url",
object.properties,
'in the "' + object.name + '" object of type "website"'
);
const allowApi = PropertyUtils.findBooleanProperty("allowApi", object.properties);
// TODO: add a "allow" property to iframe
this.embeddedWebsiteManager.createEmbeddedWebsite(
object.name,
url,
object.x,
object.y,
object.width,
object.height,
object.visible,
allowApi ?? false,
""
);
}
} }
} }
} }
@ -1166,6 +1195,27 @@ ${escapedMessage}
iframeListener.registerAnswerer("removeActionMessage", (message) => { iframeListener.registerAnswerer("removeActionMessage", (message) => {
layoutManagerActionStore.removeAction(message.uuid); layoutManagerActionStore.removeAction(message.uuid);
}); });
this.iframeSubscriptionList.push(
iframeListener.modifyEmbeddedWebsiteStream.subscribe((embeddedWebsite) => {
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
// TODO
})
);
} }
private setPropertyLayer( private setPropertyLayer(
@ -1233,7 +1283,7 @@ ${escapedMessage}
let targetRoom: Room; let targetRoom: Room;
try { try {
targetRoom = await Room.createRoom(roomUrl); targetRoom = await Room.createRoom(roomUrl);
} catch (e) { } catch (e /*: unknown*/) {
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e); console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
this.mapTransitioning = false; this.mapTransitioning = false;
return; return;
@ -1293,6 +1343,7 @@ ${escapedMessage}
iframeListener.unregisterAnswerer("triggerActionMessage"); iframeListener.unregisterAnswerer("triggerActionMessage");
iframeListener.unregisterAnswerer("removeActionMessage"); iframeListener.unregisterAnswerer("removeActionMessage");
this.sharedVariablesManager?.close(); this.sharedVariablesManager?.close();
this.embeddedWebsiteManager?.close();
mediaManager.hideGameOverlay(); mediaManager.hideGameOverlay();
@ -1364,7 +1415,7 @@ ${escapedMessage}
try { try {
const room = await Room.createRoom(exitRoomPath); const room = await Room.createRoom(exitRoomPath);
return gameManager.loadMap(room, this.scene); return gameManager.loadMap(room, this.scene);
} catch (e) { } catch (e /*: unknown*/) {
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e); console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
} }
} }

View File

@ -0,0 +1,53 @@
import type { ITiledMapProperty } from "./ITiledMap";
export class PropertyUtils {
public static findProperty(
name: string,
properties: ITiledMapProperty[] | undefined
): string | boolean | number | undefined {
return properties?.find((property) => property.name === name)?.value;
}
public static findBooleanProperty(
name: string,
properties: ITiledMapProperty[] | undefined,
context?: string
): boolean | undefined {
const property = PropertyUtils.findProperty(name, properties);
if (property === undefined) {
return undefined;
}
if (typeof property !== "boolean") {
throw new Error(
'Expected property "' + name + '" to be a boolean. ' + (context ? " (" + context + ")" : "")
);
}
return property;
}
public static mustFindProperty(
name: string,
properties: ITiledMapProperty[] | undefined,
context?: string
): string | boolean | number {
const property = PropertyUtils.findProperty(name, properties);
if (property === undefined) {
throw new Error('Could not find property "' + name + '"' + (context ? " (" + context + ")" : ""));
}
return property;
}
public static mustFindStringProperty(
name: string,
properties: ITiledMapProperty[] | undefined,
context?: string
): string {
const property = PropertyUtils.mustFindProperty(name, properties, context);
if (typeof property !== "string") {
throw new Error(
'Expected property "' + name + '" to be a string. ' + (context ? " (" + context + ")" : "")
);
}
return property;
}
}

View File

@ -385,6 +385,10 @@ body {
#game { #game {
position: relative; /* Position relative is needed for the game-overlay. */ position: relative; /* Position relative is needed for the game-overlay. */
iframe {
pointer-events: all;
}
} }
.audioplayer:first-child { .audioplayer:first-child {

View File

@ -0,0 +1,10 @@
<html>
<head>
</head>
<body>
<h1>Hello world</h1>
<p>This is a webpage integrated in your map</p>
<p>Here is a form, right in your map! <input type="text" value="because I can!"> <button>Submit!</button></p>
</body>
</html>

View File

@ -0,0 +1,99 @@
{ "compressionlevel":-1,
"height":30,
"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, 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, 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, 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, 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, 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, 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, 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, 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":30,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":30,
"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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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":30,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":30,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":83.6666666666666,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"Test:\nWalk around the map\nResult:\nYou should see a transparent website at the center of the map, that is \"fixed\" on the map floor.",
"wrap":true
},
"type":"",
"visible":true,
"width":315.4375,
"x":68.4021076998051,
"y":8.73391812865529
},
{
"height":419.805068226121,
"id":2,
"name":"demo website",
"properties":[
{
"name":"url",
"type":"string",
"value":"integrated_website_1.html"
}],
"rotation":0,
"type":"website",
"visible":true,
"width":637.504873294347,
"x":189.005847953216,
"y":156.569200779727
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":6,
"nextobjectid":3,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"..\/tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":30
}

View File

@ -0,0 +1,85 @@
<!doctype html>
<html lang="en">
<head>
<script>
var script = document.createElement('script');
// Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security.
// We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files.
script.setAttribute('src', document.referrer + 'iframe_api.js');
document.head.appendChild(script);
window.addEventListener('load', () => {
console.log('On load');
WA.onInit().then(() => {
console.log('After WA init');
const createButton = document.getElementById('createEmbeddedWebsite');
const deleteButton = document.getElementById('deleteEmbeddedWebsite');
const xField = document.getElementById('x');
const yField = document.getElementById('y');
const widthField = document.getElementById('width');
const heightField = document.getElementById('height');
const urlField = document.getElementById('url');
const visibleField = document.getElementById('visible');
createButton.addEventListener('click', () => {
console.log('CREATING NEW EMBEDDED IFRAME');
WA.room.website.create({
name: "test",
url: urlField.value,
position: {
x: parseInt(xField.value),
y: parseInt(yField.value),
width: parseInt(widthField.value),
height: parseInt(heightField.value),
},
visible: !!visibleField.value,
});
});
deleteButton.addEventListener('click', () => {
WA.room.website.delete("test");
});
xField.addEventListener('change', async function() {
const website = await WA.room.website.get('test');
website.x = parseInt(this.value);
});
yField.addEventListener('change', async function() {
const website = await WA.room.website.get('test');
website.y = parseInt(this.value);
});
widthField.addEventListener('change', async function() {
const website = await WA.room.website.get('test');
website.width = parseInt(this.value);
});
heightField.addEventListener('change', async function() {
const website = await WA.room.website.get('test');
website.height = parseInt(this.value);
});
urlField.addEventListener('change', async function() {
const website = await WA.room.website.get('test');
website.url = this.value;
});
visibleField.addEventListener('change', async function() {
const website = await WA.room.website.get('test');
website.visible = this.checked;
});
});
})
</script>
</head>
<body>
X: <input type="text" id="x" value="64" /><br/>
Y: <input type="text" id="y" value="64" /><br/>
width: <input type="text" id="width" value="600" /><br/>
height: <input type="text" id="height" value="400" /><br/>
URL: <input type="text" id="url" value="https://mensuel.framapad.org/p/rt6c904745-9oxm?lang=en" /><br/>
Visible: <input type="checkbox" id="visible" value=1 /><br/>
<button id="createEmbeddedWebsite">Create embedded website</button>
<button id="deleteEmbeddedWebsite">Delete embedded website</button>
</body>
</html>

View File

@ -0,0 +1,93 @@
{ "compressionlevel":-1,
"height":30,
"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, 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, 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, 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, 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, 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, 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, 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, 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":30,
"id":1,
"name":"floor",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"website_in_map_script.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"type":"tilelayer",
"visible":true,
"width":30,
"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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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":30,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":30,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":393,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":13,
"text":"Test:\nClick the \"create\" button.\n\nResult:\nA website should appear.\n\nTest:\nUse the fields to modify settings.\n\nResult:\nThe iframe is modified accordingly.\n\nTest:\nClick the \"delete\" button.\n\nResult:\nThe iframe is deleted\n\nTest:\nClick the \"create\" button.\n\nResult:\nA website should appear.\n",
"wrap":true
},
"type":"",
"visible":true,
"width":315.4375,
"x":68.4021076998051,
"y":8.73391812865529
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":6,
"nextobjectid":3,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"..\/tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":30
}

View File

@ -210,6 +210,22 @@
<a href="#" class="testLink" data-testmap="TriggerMessageApi/triggerMessage.json" target="_blank">Testing trigger message API</a> <a href="#" class="testLink" data-testmap="TriggerMessageApi/triggerMessage.json" target="_blank">Testing trigger message API</a>
</td> </td>
</tr> </tr>
<tr>
<td>
<input type="radio" name="test-website-objects"> Success <input type="radio" name="test-website-objects"> Failure <input type="radio" name="test-website-objects" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="EmbeddedWebsite/website_in_map.json" target="_blank">Testing websites inside a map</a>
</td>
</tr>
<tr>
<td>
<input type="radio" name="test-website-objects-script"> Success <input type="radio" name="test-website-objects-script"> Failure <input type="radio" name="test-website-objects-script" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="EmbeddedWebsite/website_in_map_script.json" target="_blank">Testing scripting API for websites inside a map</a>
</td>
</tr>
</table> </table>
<script> <script>