Skip to content

Commit da3ea61

Browse files
authored
[web export] Fix Safari Arrow keys in HTML export/webapp (#2838)
* web: Fix Safari Arrow keys in HTML export and webapp\n\n- Remap Arrow key events with KeyboardEvent.location === 3 (numpad) to location: 0 on WebKit in capture phase\n- Prevent page scrolling on Arrow keys while canvas is focused\n- Ensure canvas is focusable (tabindex=0) and focused on start\n\nThis addresses Safari/WebKit misclassification of physical Arrow keys so games receive directional input correctly. * Add missing semicolon * Update index.html * Update index.html
1 parent 8071f38 commit da3ea61

File tree

3 files changed

+176
-5
lines changed

3 files changed

+176
-5
lines changed

build/html/export.html

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,66 @@
2828

2929
</div>
3030

31-
<canvas style="width: 100%; height: 100%; margin: 0 auto; display: block; image-rendering: pixelated;" id="canvas" oncontextmenu="event.preventDefault()" onmousedown="window.focus()"></canvas>
31+
<canvas style="width: 100%; height: 100%; margin: 0 auto; display: block; image-rendering: pixelated;" id="canvas" oncontextmenu="event.preventDefault()" onmousedown="window.focus()" tabindex="0"></canvas>
3232
</div>
3333

3434
<script type="text/javascript">
35-
var Module = {canvas: document.getElementById('canvas'), arguments:['cart.tic']}
35+
var Module = {canvas: document.getElementById('canvas'), arguments:['cart.tic']};
36+
37+
// Safari/WebKit-only: remap Arrow keys with location 3 (numpad) to location 0.
38+
(function(){
39+
const ua = navigator.userAgent || '';
40+
const isWebKit = /AppleWebKit/i.test(ua);
41+
if (!isWebKit) return;
42+
43+
const synthesizeArrowWithLocation0 = (orig) => {
44+
try {
45+
const ev = new KeyboardEvent(orig.type, {
46+
key: orig.key,
47+
code: orig.code,
48+
location: 0,
49+
repeat: orig.repeat,
50+
ctrlKey: orig.ctrlKey,
51+
shiftKey: orig.shiftKey,
52+
altKey: orig.altKey,
53+
metaKey: orig.metaKey,
54+
bubbles: true,
55+
cancelable: true,
56+
});
57+
Object.defineProperty(ev, 'which', { get: () => orig.which });
58+
Object.defineProperty(ev, 'keyCode', { get: () => orig.keyCode });
59+
return ev;
60+
} catch { return null; }
61+
};
62+
63+
const remapArrowLocationCapture = (e) => {
64+
const isArrow = (e.key && e.key.startsWith('Arrow')) || (e.code && e.code.startsWith('Arrow'));
65+
if (!isArrow) return;
66+
if (e.location === 3) {
67+
const syn = synthesizeArrowWithLocation0(e);
68+
if (syn) {
69+
e.stopImmediatePropagation();
70+
e.preventDefault();
71+
(e.target || document.getElementById('canvas')).dispatchEvent(syn);
72+
}
73+
}
74+
};
75+
76+
['keydown','keyup'].forEach(t => document.addEventListener(t, remapArrowLocationCapture, true));
77+
})();
78+
79+
// Prevent page scrolling while the game canvas is focused
80+
(function(){
81+
const canvasEl = document.getElementById('canvas');
82+
const preventArrowDefault = (e) => {
83+
const key = e.key || e.code;
84+
const kc = e.keyCode || e.which;
85+
const isArrow = (key && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(key)) || (kc >= 37 && kc <= 40);
86+
if (isArrow && document.activeElement === canvasEl) e.preventDefault();
87+
};
88+
document.addEventListener('keydown', preventArrowDefault, { passive: false });
89+
canvasEl.addEventListener('keydown', preventArrowDefault, { passive: false });
90+
})();
3691

3792
const gameFrame = document.getElementById('game-frame')
3893
const displayStyle = window.getComputedStyle(gameFrame).display;
@@ -43,13 +98,17 @@
4398
firstScriptTag = document.getElementsByTagName('script')[0]; // find the first script tag in the document
4499
scriptTag.src = 'tic80.js'; // set the source of the script to your script
45100
firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); // append the script to the DOM
101+
// Focus canvas to ensure keyboard events are routed to the game
102+
document.getElementById('canvas').focus()
46103
} else {
47104
gameFrame.addEventListener('click', function() {
48105
let scriptTag = document.createElement('script'), // create a script tag
49106
firstScriptTag = document.getElementsByTagName('script')[0]; // find the first script tag in the document
50107
scriptTag.src = 'tic80.js'; // set the source of the script to your script
51108
firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); // append the script to the DOM
52109
this.remove()
110+
// Focus canvas to ensure keyboard events are routed to the game
111+
document.getElementById('canvas').focus()
53112
});
54113
}
55114
</script>

build/html/index.html

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
</div>
3333

34-
<canvas style="width: 100%; height: 100%; margin: 0 auto; display: block; image-rendering: pixelated;" id="canvas" oncontextmenu="event.preventDefault()" onmousedown="window.focus()"></canvas>
34+
<canvas style="width: 100%; height: 100%; margin: 0 auto; display: block; image-rendering: pixelated;" id="canvas" oncontextmenu="event.preventDefault()" onmousedown="window.focus()" tabindex="0"></canvas>
3535
</div>
3636

3737
<div id="add-modal" class="modal">
@@ -43,7 +43,62 @@
4343
</div>
4444

4545
<script type="text/javascript">
46-
var Module = {canvas: document.getElementById('canvas')}
46+
var Module = {canvas: document.getElementById('canvas')};
47+
48+
// Safari/WebKit-only: remap Arrow keys with location 3 (numpad) to location 0.
49+
(function(){
50+
const ua = navigator.userAgent || '';
51+
const isWebKit = /AppleWebKit/i.test(ua);
52+
if (!isWebKit) return;
53+
54+
const synthesizeArrowWithLocation0 = (orig) => {
55+
try {
56+
const ev = new KeyboardEvent(orig.type, {
57+
key: orig.key,
58+
code: orig.code,
59+
location: 0,
60+
repeat: orig.repeat,
61+
ctrlKey: orig.ctrlKey,
62+
shiftKey: orig.shiftKey,
63+
altKey: orig.altKey,
64+
metaKey: orig.metaKey,
65+
bubbles: true,
66+
cancelable: true,
67+
});
68+
Object.defineProperty(ev, 'which', { get: () => orig.which });
69+
Object.defineProperty(ev, 'keyCode', { get: () => orig.keyCode });
70+
return ev;
71+
} catch { return null; }
72+
};
73+
74+
const remapArrowLocationCapture = (e) => {
75+
const isArrow = (e.key && e.key.startsWith('Arrow')) || (e.code && e.code.startsWith('Arrow'));
76+
if (!isArrow) return;
77+
if (e.location === 3) {
78+
const syn = synthesizeArrowWithLocation0(e);
79+
if (syn) {
80+
e.stopImmediatePropagation();
81+
e.preventDefault();
82+
(e.target || document.getElementById('canvas')).dispatchEvent(syn);
83+
}
84+
}
85+
};
86+
87+
['keydown','keyup'].forEach(t => document.addEventListener(t, remapArrowLocationCapture, true));
88+
})();
89+
90+
// Prevent page scrolling while the game canvas is focused
91+
(function(){
92+
const canvasEl = document.getElementById('canvas');
93+
const preventArrowDefault = (e) => {
94+
const key = e.key || e.code;
95+
const kc = e.keyCode || e.which;
96+
const isArrow = (key && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(key)) || (kc >= 37 && kc <= 40);
97+
if (isArrow && document.activeElement === canvasEl) e.preventDefault();
98+
};
99+
document.addEventListener('keydown', preventArrowDefault, { passive: false });
100+
canvasEl.addEventListener('keydown', preventArrowDefault, { passive: false });
101+
})();
47102

48103
const gameFrame = document.getElementById('game-frame')
49104

@@ -53,6 +108,8 @@
53108
scriptTag.src = 'tic80.js'; // set the source of the script to your script
54109
firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); // append the script to the DOM
55110
this.remove()
111+
// Focus canvas to ensure keyboard events are routed to the game
112+
document.getElementById('canvas').focus()
56113
});
57114
</script>
58115
</body>

build/webapp/index.html

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,62 @@
3232
<script type="module">
3333
import startTic80 from './tic80.js'
3434

35-
navigator.serviceWorker.register('serviceworker.js')
35+
navigator.serviceWorker.register('serviceworker.js');
36+
37+
// Safari/WebKit-only: remap Arrow keys with location 3 (numpad) to location 0.
38+
(function(){
39+
const ua = navigator.userAgent || '';
40+
const isWebKit = /AppleWebKit/i.test(ua);
41+
if (!isWebKit) return;
42+
43+
const synthesizeArrowWithLocation0 = (orig) => {
44+
try {
45+
const ev = new KeyboardEvent(orig.type, {
46+
key: orig.key,
47+
code: orig.code,
48+
location: 0,
49+
repeat: orig.repeat,
50+
ctrlKey: orig.ctrlKey,
51+
shiftKey: orig.shiftKey,
52+
altKey: orig.altKey,
53+
metaKey: orig.metaKey,
54+
bubbles: true,
55+
cancelable: true,
56+
});
57+
Object.defineProperty(ev, 'which', { get: () => orig.which });
58+
Object.defineProperty(ev, 'keyCode', { get: () => orig.keyCode });
59+
return ev;
60+
} catch { return null; }
61+
};
62+
63+
const remapArrowLocationCapture = (e) => {
64+
const isArrow = (e.key && e.key.startsWith('Arrow')) || (e.code && e.code.startsWith('Arrow'));
65+
if (!isArrow) return;
66+
if (e.location === 3) {
67+
const syn = synthesizeArrowWithLocation0(e);
68+
if (syn) {
69+
e.stopImmediatePropagation();
70+
e.preventDefault();
71+
(e.target || document.getElementById('canvas')).dispatchEvent(syn);
72+
}
73+
}
74+
};
75+
76+
['keydown','keyup'].forEach(t => document.addEventListener(t, remapArrowLocationCapture, true));
77+
})();
78+
79+
// Prevent page scrolling while the canvas is focused
80+
(function(){
81+
const canvasEl = document.getElementById('canvas');
82+
const preventArrowDefault = (e) => {
83+
const key = e.key || e.code;
84+
const kc = e.keyCode || e.which;
85+
const isArrow = (key && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(key)) || (kc >= 37 && kc <= 40);
86+
if (isArrow && document.activeElement === canvasEl) e.preventDefault();
87+
};
88+
document.addEventListener('keydown', preventArrowDefault, { passive: false });
89+
canvasEl.addEventListener('keydown', preventArrowDefault, { passive: false });
90+
})();
3691

3792
const initialize = () => {
3893
const canvasSelector = '#canvas'

0 commit comments

Comments
 (0)