From ce3c53ae3fcbc2fcaf94177d35e0e6525f8a79fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 23 Jul 2021 20:22:45 +0200 Subject: [PATCH] Adding the ability to inject websites directly inside maps This PR adds the ability to inject a website INSIDE a map (as an iframe inside a Phaser HTML object) The iFrame will be rendered transparently, unless you set a background-color on the body, which opens a number of cool possibilities. Needs to be done: allowing the iframe API in those iframes. --- docs/maps/website-in-map.md | 24 +++++++ front/src/Phaser/Game/GameScene.ts | 31 +++++++-- front/src/Phaser/Map/PropertyUtils.ts | 36 ++++++++++ front/style/style.scss | 4 ++ maps/tests/index.html | 8 +++ maps/tests/integrated_website_1.html | 10 +++ maps/tests/website_in_map.json | 99 +++++++++++++++++++++++++++ 7 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 docs/maps/website-in-map.md create mode 100644 front/src/Phaser/Map/PropertyUtils.ts create mode 100644 maps/tests/integrated_website_1.html create mode 100644 maps/tests/website_in_map.json diff --git a/docs/maps/website-in-map.md b/docs/maps/website-in-map.md new file mode 100644 index 00000000..849a30e0 --- /dev/null +++ b/docs/maps/website-in-map.md @@ -0,0 +1,24 @@ +{.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. diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 0ea62a4f..596fcc58 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -84,6 +84,7 @@ 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"; @@ -198,6 +199,7 @@ export class GameScene extends DirtyScene { private preloading: boolean = true; private startPositionCalculator!: StartPositionCalculator; private sharedVariablesManager!: SharedVariablesManager; + private objectsByType = new Map(); constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ @@ -337,27 +339,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; @@ -477,6 +479,25 @@ 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 absoluteUrl = new URL(url, this.MapUrlFile).toString(); + + 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); + } } } } diff --git a/front/src/Phaser/Map/PropertyUtils.ts b/front/src/Phaser/Map/PropertyUtils.ts new file mode 100644 index 00000000..a23ef269 --- /dev/null +++ b/front/src/Phaser/Map/PropertyUtils.ts @@ -0,0 +1,36 @@ +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 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/index.html b/maps/tests/index.html index 74c13891..e078fe9c 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -210,6 +210,14 @@ Testing trigger message API + + + Success Failure Pending + + + Testing websites inside a map + +