Skip to content

Commit

Permalink
fix: [#1512] Grab window focus by default for x-origin iframes (#2660)
Browse files Browse the repository at this point in the history
This PR adds a new engine option (which is on by default) to grab the window focus when hosted in an iframe. This allows keyboard events to work by default.

Confirmed on itch.io https://eonarheim.itch.io/iframe-test

Closes #1512
  • Loading branch information
eonarheim authored Jun 14, 2023
1 parent 848f1da commit 799ff70
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 26 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ are returned

### Fixed

- Fixed issue when excalibur was hosted in a x-origin iframe, the engine will grab window focus by default if in an iframe. This can be suppressed with `new ex.Engine({grabWindowFocus: false})`
- Fixed issue where `ex.Camera.rotation = ...` did not work to rotate the camera, also addressed offscreen culling issues that were revealed by this fix.
- Fixed issue where the `ex.ScreenElement` anchor was not being accounted for properly when passed as a constructor parameter.
- Fixed issue where you could not use multiple instances of Excalibur on the same page, you can now have as many Excalibur's as you want (up to the webgl context limit).
Expand Down
13 changes: 13 additions & 0 deletions sandbox/tests/iframe/frame.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Frame</title>
</head>
<body>
<script src="../../lib/excalibur.js"></script>
<script src="./main.js"></script>
</body>
</html>
13 changes: 13 additions & 0 deletions sandbox/tests/iframe/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button>Other things that have focus</button>
<iframe src="frame.html" width="600" height="400" frameborder="0"></iframe>
</body>
</html>
21 changes: 21 additions & 0 deletions sandbox/tests/iframe/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// window.focus();
// document.body.addEventListener('keydown', (evt) => {
// console.log(evt.code);
// })


var engine = new ex.Engine({
width: 600,
height: 400
});

engine.input.keyboard.on('press', (evt) => {
console.log(evt.key);
});

engine.onPostDraw = (ctx: ex.ExcaliburGraphicsContext) => {
const keys = engine.input.keyboard.getKeys();
ctx.debug.drawText(keys.join(','), ex.vec(200, 200));
}

engine.start();
18 changes: 16 additions & 2 deletions src/engine/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ export interface EngineOptions {
*/
suppressPlayButton?: boolean;

/**
* Sets the focus of the window, this is needed when hosting excalibur in a cross-origin iframe in order for certain events
* (like keyboard) to work.
* For example: itch.io or codesandbox.io
*
* By default set to true,
*/
grabWindowFocus?: boolean;

/**
* Scroll prevention method.
*/
Expand Down Expand Up @@ -547,6 +556,7 @@ export class Engine extends Class implements CanInitialize, CanUpdate, CanDraw {
suppressMinimumBrowserFeatureDetection: null,
suppressHiDPIScaling: null,
suppressPlayButton: null,
grabWindowFocus: true,
scrollPreventionMode: ScrollPreventionMode.Canvas,
backgroundColor: Color.fromHex('#2185d0') // Excalibur blue
};
Expand Down Expand Up @@ -1102,8 +1112,12 @@ O|===|* >________________>\n\
pointers: new PointerEventReceiver(pointerTarget, this),
gamepads: new Input.Gamepads()
};
this.input.keyboard.init();
this.input.pointers.init();
this.input.keyboard.init({
grabWindowFocus: this._originalOptions?.grabWindowFocus ?? true
});
this.input.pointers.init({
grabWindowFocus: this._originalOptions?.grabWindowFocus ?? true
});
this.input.gamepads.init();

// Issue #385 make use of the visibility api
Expand Down
41 changes: 19 additions & 22 deletions src/engine/Input/Keyboard.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Logger } from '../Util/Log';
import { Class } from '../Class';
import * as Events from '../Events';
import { isCrossOriginIframe } from '../Util/IFrame';

/**
* Enum representing physical input key codes
Expand Down Expand Up @@ -189,6 +190,11 @@ export class KeyEvent extends Events.GameEvent<any> {
}
}

export interface KeyboardInitOptions {
global?: GlobalEventHandlers,
grabWindowFocus?: boolean
}

/**
* Provides keyboard support for Excalibur.
*/
Expand All @@ -212,31 +218,22 @@ export class Keyboard extends Class {
/**
* Initialize Keyboard event listeners
*/
init(global?: GlobalEventHandlers): void {
init(keyboardOptions?: KeyboardInitOptions): void {
let { global } = keyboardOptions;
const { grabWindowFocus } = keyboardOptions;
if (!global) {
try {
// Try and listen to events on top window frame if within an iframe.
//
// See https://github.com/excaliburjs/Excalibur/issues/1294
//
// Attempt to add an event listener, which triggers a DOMException on
// cross-origin iframes
const noop = () => {
return;
};
window.top.addEventListener('blur', noop);
window.top.removeEventListener('blur', noop);

// this will be the same as window if not embedded within an iframe
global = window.top;
} catch {
// fallback to current frame
if (isCrossOriginIframe()) {
global = window;
// Workaround for iframes like for itch.io or codesandbox
// https://www.reddit.com/r/gamemaker/comments/kfs5cs/keyboard_inputs_no_longer_working_in_html5_game/
// https://forum.gamemaker.io/index.php?threads/solved-keyboard-issue-on-itch-io.87336/
if (grabWindowFocus) {
window.focus();
}

Logger.getInstance().warn(
'Failed to bind to keyboard events to top frame. ' +
'If you are trying to embed Excalibur in a cross-origin iframe, keyboard events will not fire.'
);
Logger.getInstance().warn('Excalibur might be in a cross-origin iframe, in order to receive keyboard events it must be in focus');
} else {
global = window.top;
}
}

Expand Down
25 changes: 24 additions & 1 deletion src/engine/Input/PointerEventReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { NativePointerButton } from './NativePointerButton';
import { PointerButton } from './PointerButton';
import { fail } from '../Util/Util';
import { PointerType } from './PointerType';
import { isCrossOriginIframe } from '../Util/IFrame';


export type NativePointerEvent = globalThis.PointerEvent;
Expand All @@ -35,6 +36,10 @@ function isPointerEvent(value: any): value is NativePointerEvent {
return globalThis.PointerEvent && value instanceof globalThis.PointerEvent;
}

export interface PointerInitOptions {
grabWindowFocus?: boolean;
}

/**
* The PointerEventProcessor is responsible for collecting all the events from the canvas and transforming them into GlobalCoordinates
*/
Expand Down Expand Up @@ -222,7 +227,7 @@ export class PointerEventReceiver extends Class {
* Initializes the pointer event receiver so that it can start listening to native
* browser events.
*/
public init() {
public init(options?: PointerInitOptions) {
// Disabling the touch action avoids browser/platform gestures from firing on the canvas
// It is important on mobile to have touch action 'none'
// https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not
Expand Down Expand Up @@ -267,6 +272,24 @@ export class PointerEventReceiver extends Class {
// Remaining browser and older Firefox
this.target.addEventListener('MozMousePixelScroll', this._boundWheel, wheelOptions);
}

const grabWindowFocus = options?.grabWindowFocus ?? true;
// Handle cross origin iframe
if (grabWindowFocus && isCrossOriginIframe()) {
const grabFocus = () => {
window.focus();
};
// Preferred pointer events
if (window.PointerEvent) {
this.target.addEventListener('pointerdown', grabFocus);
} else {
// Touch Events
this.target.addEventListener('touchstart', grabFocus);

// Mouse Events
this.target.addEventListener('mousedown', grabFocus);
}
}
}

public detach() {
Expand Down
23 changes: 23 additions & 0 deletions src/engine/Util/IFrame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@


/**
* Checks if excalibur is in a x-origin iframe
*/
export function isCrossOriginIframe() {
try {
// Try and listen to events on top window frame if within an iframe.
//
// See https://github.com/excaliburjs/Excalibur/issues/1294
//
// Attempt to add an event listener, which triggers a DOMException on
// cross-origin iframes
const noop = () => {
return;
};
window.top.addEventListener('blur', noop);
window.top.removeEventListener('blur', noop);
} catch {
return true;
}
return false;
}
2 changes: 1 addition & 1 deletion src/spec/KeyInputSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('A keyboard', () => {
beforeEach(() => {
mockWindow = <any>mocker.window();
keyboard = new ex.Input.Keyboard();
keyboard.init(mockWindow);
keyboard.init({global: mockWindow});
});

it('should exist', () => {
Expand Down

0 comments on commit 799ff70

Please sign in to comment.