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/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 8f33d8e5..68aad68d 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -89,6 +89,7 @@ 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; @@ -200,7 +201,7 @@ export class GameScene extends DirtyScene { private startPositionCalculator!: StartPositionCalculator; private sharedVariablesManager!: SharedVariablesManager; private objectsByType = new Map(); - private inMapIframes = new Array(); + private embeddedWebsiteManager!: EmbeddedWebsiteManager; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ @@ -419,6 +420,7 @@ export class GameScene extends DirtyScene { //hook create scene create(): void { + console.log("GAAAAAAAGAGAGAGAGA"); this.preloading = false; this.trackDirtyAnims(); @@ -460,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) { @@ -487,26 +491,20 @@ export class GameScene extends DirtyScene { object.properties, 'in the "' + object.name + '" object of type "website"' ); - const absoluteUrl = new URL(url, this.MapUrlFile).toString(); + const allowApi = PropertyUtils.findBooleanProperty("allowApi", object.properties); - const iframe = document.createElement("iframe"); - iframe.src = absoluteUrl; - iframe.style.width = object.width + "px"; - iframe.style.height = object.height + "px"; - iframe.style.margin = "0"; - iframe.style.padding = "0"; - iframe.style.border = "none"; - - this.add.dom(object.x, object.y).setElement(iframe).setOrigin(0, 0); - - 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, + "" ); - if (allowApi) { - iframeListener.registerIframe(iframe); - this.inMapIframes.push(iframe); - } } } } @@ -1197,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( @@ -1264,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; @@ -1303,9 +1322,6 @@ ${escapedMessage} for (const script of scripts) { iframeListener.unregisterScript(script); } - for (const iframe of this.inMapIframes) { - iframeListener.unregisterIframe(iframe); - } this.stopJitsi(); audioManager.unloadAudio(); @@ -1327,6 +1343,7 @@ ${escapedMessage} iframeListener.unregisterAnswerer("triggerActionMessage"); iframeListener.unregisterAnswerer("removeActionMessage"); this.sharedVariablesManager?.close(); + this.embeddedWebsiteManager?.close(); mediaManager.hideGameOverlay(); @@ -1398,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/maps/tests/integrated_website_1.html b/maps/tests/EmbeddedWebsite/integrated_website_1.html similarity index 100% rename from maps/tests/integrated_website_1.html rename to maps/tests/EmbeddedWebsite/integrated_website_1.html diff --git a/maps/tests/website_in_map.json b/maps/tests/EmbeddedWebsite/website_in_map.json similarity index 99% rename from maps/tests/website_in_map.json rename to maps/tests/EmbeddedWebsite/website_in_map.json index 7a7c7c51..59ada784 100644 --- a/maps/tests/website_in_map.json +++ b/maps/tests/EmbeddedWebsite/website_in_map.json @@ -82,7 +82,7 @@ { "columns":11, "firstgid":1, - "image":"tileset1.png", + "image":"..\/tileset1.png", "imageheight":352, "imagewidth":352, "margin":0, 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 e078fe9c..332875b9 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -215,7 +215,15 @@ Success Failure Pending - Testing websites inside a map + Testing websites inside a map + + + + + Success Failure Pending + + + Testing scripting API for websites inside a map