diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md index 579b3f58..96b62fd2 100644 --- a/docs/maps/api-room.md +++ b/docs/maps/api-room.md @@ -176,4 +176,102 @@ You can create a tileset file in Tile Editor. WA.room.loadTileset("Assets/Tileset.json").then((firstId) => { WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]); }) -``` \ No newline at end of file +``` + + +## 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 +``` + +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 +``` + +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. + diff --git a/docs/maps/website-in-map.md b/docs/maps/website-in-map.md new file mode 100644 index 00000000..7c7f8025 --- /dev/null +++ b/docs/maps/website-in-map.md @@ -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 + +
+
+ +
A "website" object
+
+
+ +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`. + +
+
+ +
A "website" object that can communicate using the Iframe API
+
+
diff --git a/front/src/Api/Events/EmbeddedWebsiteEvent.ts b/front/src/Api/Events/EmbeddedWebsiteEvent.ts new file mode 100644 index 00000000..42630be1 --- /dev/null +++ b/front/src/Api/Events/EmbeddedWebsiteEvent.ts @@ -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; + +export type CreateEmbeddedWebsiteEvent = tg.GuardedType; +// TODO: make a variation that is all optional (except for the name) +export type Rectangle = tg.GuardedType; diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index ca1d9cc3..ed723241 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -22,6 +22,8 @@ import type { SetVariableEvent } from "./SetVariableEvent"; import { isGameStateEvent } from "./GameStateEvent"; import { isMapDataEvent } from "./MapDataEvent"; import { isSetVariableEvent } from "./SetVariableEvent"; +import type { EmbeddedWebsite } from "../iframe/Room/EmbeddedWebsite"; +import { isCreateEmbeddedWebsiteEvent } from "./EmbeddedWebsiteEvent"; import type { LoadTilesetEvent } from "./LoadTilesetEvent"; import { isLoadTilesetEvent } from "./LoadTilesetEvent"; import type { @@ -63,6 +65,7 @@ export type IframeEventMap = { loadTileset: LoadTilesetEvent; registerMenuCommand: MenuItemRegisterEvent; setTiles: SetTilesEvent; + modifyEmbeddedWebsite: Partial; // Note: name should be compulsory in fact }; export interface IframeEvent { type: T; @@ -122,6 +125,18 @@ export const iframeQueryMapTypeGuards = { query: isMessageReferenceEvent, 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 extends (x: unknown) => x is infer T ? T : never; @@ -158,7 +173,12 @@ export const isIframeQuery = (event: any): event is IframeQuery = ( query: IframeQueryMap[T]["query"], @@ -109,6 +111,9 @@ class IframeListener { private readonly _setTilesStream: Subject = new Subject(); public readonly setTilesStream = this._setTilesStream.asObservable(); + private readonly _modifyEmbeddedWebsiteStream: Subject = new Subject(); + public readonly modifyEmbeddedWebsiteStream = this._modifyEmbeddedWebsiteStream.asObservable(); + private readonly iframes = new Set(); private readonly iframeCloseCallbacks = new Map void)[]>(); private readonly scripts = new Map(); @@ -264,6 +269,8 @@ class IframeListener { handleMenuItemRegistrationEvent(payload.data); } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { this._setTilesStream.next(payload.data); + } else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) { + this._modifyEmbeddedWebsiteStream.next(payload.data); } } }, diff --git a/front/src/Api/iframe/Room/EmbeddedWebsite.ts b/front/src/Api/iframe/Room/EmbeddedWebsite.ts new file mode 100644 index 00000000..7b16890e --- /dev/null +++ b/front/src/Api/iframe/Room/EmbeddedWebsite.ts @@ -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, + }, + }); + } +} diff --git a/front/src/Api/iframe/room.ts b/front/src/Api/iframe/room.ts index 9c0be9be..22df49c9 100644 --- a/front/src/Api/iframe/room.ts +++ b/front/src/Api/iframe/room.ts @@ -6,6 +6,8 @@ import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from " import { apiCallback } from "./registeredCallbacks"; import type { ITiledMap } from "../../Phaser/Map/ITiledMap"; +import type { WorkadventureRoomWebsiteCommands } from "./website"; +import website from "./website"; const enterStreams: Map> = new Map>(); const leaveStreams: Map> = new Map>(); @@ -105,6 +107,7 @@ export class WorkadventureRoomCommands extends IframeApiContribution { return await queryWorkadventure({ type: "loadTileset", @@ -113,6 +116,10 @@ export class WorkadventureRoomCommands extends IframeApiContribution { + callbacks = []; + + async get(objectName: string): Promise { + 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 { + return await queryWorkadventure({ + type: "deleteEmbeddedWebsite", + data: objectName, + }); + } +} + +export default new WorkadventureRoomWebsiteCommands(); diff --git a/front/src/Phaser/Game/EmbeddedWebsiteManager.ts b/front/src/Phaser/Game/EmbeddedWebsiteManager.ts new file mode 100644 index 00000000..21a38ee5 --- /dev/null +++ b/front/src/Phaser/Game/EmbeddedWebsiteManager.ts @@ -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(); + 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"); + } +} diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 0ea62a4f..68aad68d 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -84,10 +84,12 @@ import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStor import { SharedVariablesManager } from "./SharedVariablesManager"; import { playersStore } from "../../Stores/PlayersStore"; import { chatVisibilityStore } from "../../Stores/ChatStore"; +import { PropertyUtils } from "../Map/PropertyUtils"; import Tileset = Phaser.Tilemaps.Tileset; import { userIsAdminStore } from "../../Stores/GameStore"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { get } from "svelte/store"; +import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; export interface GameSceneInitInterface { initPosition: PointInterface | null; @@ -198,6 +200,8 @@ export class GameScene extends DirtyScene { private preloading: boolean = true; private startPositionCalculator!: StartPositionCalculator; private sharedVariablesManager!: SharedVariablesManager; + private objectsByType = new Map(); + private embeddedWebsiteManager!: EmbeddedWebsiteManager; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ @@ -337,27 +341,27 @@ export class GameScene extends DirtyScene { }); // Scan the object layers for objects to load and load them. - const objects = new Map(); + this.objectsByType = new Map(); for (const layer of this.mapFile.layers) { if (layer.type === "objectgroup") { for (const object of layer.objects) { let objectsOfType: ITiledMapObject[] | undefined; - if (!objects.has(object.type)) { + if (!this.objectsByType.has(object.type)) { objectsOfType = new Array(); } else { - objectsOfType = objects.get(object.type); + objectsOfType = this.objectsByType.get(object.type); if (objectsOfType === undefined) { throw new Error("Unexpected object type not found"); } } 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. let itemFactory: ItemFactoryInterface; @@ -416,6 +420,7 @@ export class GameScene extends DirtyScene { //hook create scene create(): void { + console.log("GAAAAAAAGAGAGAGAGA"); this.preloading = false; this.trackDirtyAnims(); @@ -457,6 +462,8 @@ export class GameScene extends DirtyScene { //permit to set bound collision this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); + this.embeddedWebsiteManager = new EmbeddedWebsiteManager(this); + //add layer on map this.gameMap = new GameMap(this.mapFile, this.Map, this.Terrains); for (const layer of this.gameMap.flatLayers) { @@ -477,6 +484,28 @@ export class GameScene extends DirtyScene { if (object.text) { 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) => { 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( @@ -1233,7 +1283,7 @@ ${escapedMessage} let targetRoom: Room; try { targetRoom = await Room.createRoom(roomUrl); - } catch (e) { + } catch (e /*: unknown*/) { console.error('Error while fetching new room "' + roomUrl.toString() + '"', e); this.mapTransitioning = false; return; @@ -1293,6 +1343,7 @@ ${escapedMessage} iframeListener.unregisterAnswerer("triggerActionMessage"); iframeListener.unregisterAnswerer("removeActionMessage"); this.sharedVariablesManager?.close(); + this.embeddedWebsiteManager?.close(); mediaManager.hideGameOverlay(); @@ -1364,7 +1415,7 @@ ${escapedMessage} try { const room = await Room.createRoom(exitRoomPath); return gameManager.loadMap(room, this.scene); - } catch (e) { + } catch (e /*: unknown*/) { console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e); } } diff --git a/front/src/Phaser/Map/PropertyUtils.ts b/front/src/Phaser/Map/PropertyUtils.ts new file mode 100644 index 00000000..b9aff8d7 --- /dev/null +++ b/front/src/Phaser/Map/PropertyUtils.ts @@ -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; + } +} diff --git a/front/style/style.scss b/front/style/style.scss index 24da5a96..1ed115d2 100644 --- a/front/style/style.scss +++ b/front/style/style.scss @@ -385,6 +385,10 @@ body { #game { position: relative; /* Position relative is needed for the game-overlay. */ + + iframe { + pointer-events: all; + } } .audioplayer:first-child { diff --git a/maps/tests/EmbeddedWebsite/integrated_website_1.html b/maps/tests/EmbeddedWebsite/integrated_website_1.html new file mode 100644 index 00000000..e85f5e78 --- /dev/null +++ b/maps/tests/EmbeddedWebsite/integrated_website_1.html @@ -0,0 +1,10 @@ + + + + +

Hello world

+

This is a webpage integrated in your map

+ +

Here is a form, right in your map!

+ + diff --git a/maps/tests/EmbeddedWebsite/website_in_map.json b/maps/tests/EmbeddedWebsite/website_in_map.json new file mode 100644 index 00000000..59ada784 --- /dev/null +++ b/maps/tests/EmbeddedWebsite/website_in_map.json @@ -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 +} \ No newline at end of file diff --git a/maps/tests/EmbeddedWebsite/website_in_map_script.html b/maps/tests/EmbeddedWebsite/website_in_map_script.html new file mode 100644 index 00000000..1180a2de --- /dev/null +++ b/maps/tests/EmbeddedWebsite/website_in_map_script.html @@ -0,0 +1,85 @@ + + + + + + +X:
+Y:
+width:
+height:
+URL:
+Visible:
+ + + + + + + diff --git a/maps/tests/EmbeddedWebsite/website_in_map_script.json b/maps/tests/EmbeddedWebsite/website_in_map_script.json new file mode 100644 index 00000000..00ce95cb --- /dev/null +++ b/maps/tests/EmbeddedWebsite/website_in_map_script.json @@ -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 +} \ No newline at end of file diff --git a/maps/tests/index.html b/maps/tests/index.html index 74c13891..332875b9 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -210,6 +210,22 @@ Testing trigger message API + + + Success Failure Pending + + + Testing websites inside a map + + + + + Success Failure Pending + + + Testing scripting API for websites inside a map + +