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
+