• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 15354777440

30 May 2025 08:03PM UTC coverage: 87.858% (-1.5%) from 89.344%
15354777440

Pull #3385

github

web-flow
Merge a00f57733 into e6ec66358
Pull Request #3385: updated Meet action to add tolerance

5002 of 6948 branches covered (71.99%)

3 of 5 new or added lines in 2 files covered. (60.0%)

872 existing lines in 83 files now uncovered.

13661 of 15549 relevant lines covered (87.86%)

25187.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.92
/src/engine/Input/Keyboard.ts
1
import { Logger } from '../Util/Log';
2
import * as Events from '../Events';
3
import { isCrossOriginIframe } from '../Util/IFrame';
4
import type { EventKey, Handler, Subscription } from '../EventEmitter';
5
import { EventEmitter } from '../EventEmitter';
6

7
/**
8
 * Enum representing physical input key codes
9
 *
10
 * Spec: https://w3c.github.io/uievents-code/#key-alphanumeric-section
11
 */
12
export enum Keys {
117✔
13
  // Writing System Keys https://w3c.github.io/uievents-code/#key-alphanumeric-writing-system
14
  Backquote = 'Backquote',
117✔
15
  Backslash = 'Backslash',
117✔
16
  BracketLeft = 'BracketLeft',
117✔
17
  BracketRight = 'BracketRight',
117✔
18
  Comma = 'Comma',
117✔
19

20
  // NUMBERS
21
  Key0 = 'Digit0',
117✔
22
  Key1 = 'Digit1',
117✔
23
  Key2 = 'Digit2',
117✔
24
  Key3 = 'Digit3',
117✔
25
  Key4 = 'Digit4',
117✔
26
  Key5 = 'Digit5',
117✔
27
  Key6 = 'Digit6',
117✔
28
  Key7 = 'Digit7',
117✔
29
  Key8 = 'Digit8',
117✔
30
  Key9 = 'Digit9',
117✔
31
  Digit0 = 'Digit0',
117✔
32
  Digit1 = 'Digit1',
117✔
33
  Digit2 = 'Digit2',
117✔
34
  Digit3 = 'Digit3',
117✔
35
  Digit4 = 'Digit4',
117✔
36
  Digit5 = 'Digit5',
117✔
37
  Digit6 = 'Digit6',
117✔
38
  Digit7 = 'Digit7',
117✔
39
  Digit8 = 'Digit8',
117✔
40
  Digit9 = 'Digit9',
117✔
41

42
  Equal = 'Equal',
117✔
43

44
  IntlBackslash = 'IntlBackslash',
117✔
45
  IntlRo = 'IntlRo',
117✔
46
  IntlYen = 'IntlYen',
117✔
47

48
  // LETTERS
49
  A = 'KeyA',
117✔
50
  B = 'KeyB',
117✔
51
  C = 'KeyC',
117✔
52
  D = 'KeyD',
117✔
53
  E = 'KeyE',
117✔
54
  F = 'KeyF',
117✔
55
  G = 'KeyG',
117✔
56
  H = 'KeyH',
117✔
57
  I = 'KeyI',
117✔
58
  J = 'KeyJ',
117✔
59
  K = 'KeyK',
117✔
60
  L = 'KeyL',
117✔
61
  M = 'KeyM',
117✔
62
  N = 'KeyN',
117✔
63
  O = 'KeyO',
117✔
64
  P = 'KeyP',
117✔
65
  Q = 'KeyQ',
117✔
66
  R = 'KeyR',
117✔
67
  S = 'KeyS',
117✔
68
  T = 'KeyT',
117✔
69
  U = 'KeyU',
117✔
70
  V = 'KeyV',
117✔
71
  W = 'KeyW',
117✔
72
  X = 'KeyX',
117✔
73
  Y = 'KeyY',
117✔
74
  Z = 'KeyZ',
117✔
75
  KeyA = 'KeyA',
117✔
76
  KeyB = 'KeyB',
117✔
77
  KeyC = 'KeyC',
117✔
78
  KeyD = 'KeyD',
117✔
79
  KeyE = 'KeyE',
117✔
80
  KeyF = 'KeyF',
117✔
81
  KeyG = 'KeyG',
117✔
82
  KeyH = 'KeyH',
117✔
83
  KeyI = 'KeyI',
117✔
84
  KeyJ = 'KeyJ',
117✔
85
  KeyK = 'KeyK',
117✔
86
  KeyL = 'KeyL',
117✔
87
  KeyM = 'KeyM',
117✔
88
  KeyN = 'KeyN',
117✔
89
  KeyO = 'KeyO',
117✔
90
  KeyP = 'KeyP',
117✔
91
  KeyQ = 'KeyQ',
117✔
92
  KeyR = 'KeyR',
117✔
93
  KeyS = 'KeyS',
117✔
94
  KeyT = 'KeyT',
117✔
95
  KeyU = 'KeyU',
117✔
96
  KeyV = 'KeyV',
117✔
97
  KeyW = 'KeyW',
117✔
98
  KeyX = 'KeyX',
117✔
99
  KeyY = 'KeyY',
117✔
100
  KeyZ = 'KeyZ',
117✔
101

102
  // SYMBOLS
103
  Minus = 'Minus',
117✔
104
  Period = 'Period',
117✔
105
  Quote = 'Quote',
117✔
106
  Semicolon = 'Semicolon',
117✔
107
  Slash = 'Slash',
117✔
108

109
  // Functional keys https://w3c.github.io/uievents-code/#key-alphanumeric-functional
110
  AltLeft = 'AltLeft',
117✔
111
  AltRight = 'AltRight',
117✔
112
  Alt = 'Alt',
117✔
113
  AltGraph = 'AltGraph',
117✔
114
  Backspace = 'Backspace',
117✔
115
  CapsLock = 'CapsLock',
117✔
116
  ContextMenu = 'ContextMenu',
117✔
117
  ControlLeft = 'ControlLeft',
117✔
118
  ControlRight = 'ControlRight',
117✔
119
  Enter = 'Enter',
117✔
120
  MetaLeft = 'MetaLeft',
117✔
121
  MetaRight = 'MetaRight',
117✔
122
  ShiftLeft = 'ShiftLeft',
117✔
123
  ShiftRight = 'ShiftRight',
117✔
124
  Space = 'Space',
117✔
125
  Tab = 'Tab',
117✔
126

127
  Convert = 'Convert',
117✔
128
  KanaMode = 'KanaMode',
117✔
129
  NonConvert = 'NonConvert',
117✔
130

131
  // Control Pad https://w3c.github.io/uievents-code/#key-controlpad-section
132
  Delete = 'Delete',
117✔
133
  End = 'End',
117✔
134
  Help = 'Help',
117✔
135
  Home = 'Home',
117✔
136
  Insert = 'Insert',
117✔
137
  PageDown = 'PageDown',
117✔
138
  PageUp = 'PageUp',
117✔
139

140
  // Arrow Pad https://w3c.github.io/uievents-code/#key-arrowpad-section
141
  Up = 'ArrowUp',
117✔
142
  Down = 'ArrowDown',
117✔
143
  Left = 'ArrowLeft',
117✔
144
  Right = 'ArrowRight',
117✔
145

146
  ArrowUp = 'ArrowUp',
117✔
147
  ArrowDown = 'ArrowDown',
117✔
148
  ArrowLeft = 'ArrowLeft',
117✔
149
  ArrowRight = 'ArrowRight',
117✔
150

151
  // Numpad Section https://w3c.github.io/uievents-code/#key-numpad-section
152
  NumLock = 'NumLock',
117✔
153
  Numpad0 = 'Numpad0',
117✔
154
  Numpad1 = 'Numpad1',
117✔
155
  Numpad2 = 'Numpad2',
117✔
156
  Numpad3 = 'Numpad3',
117✔
157
  Numpad4 = 'Numpad4',
117✔
158
  Numpad5 = 'Numpad5',
117✔
159
  Numpad6 = 'Numpad6',
117✔
160
  Numpad7 = 'Numpad7',
117✔
161
  Numpad8 = 'Numpad8',
117✔
162
  Numpad9 = 'Numpad9',
117✔
163
  Num0 = 'Numpad0',
117✔
164
  Num1 = 'Numpad1',
117✔
165
  Num2 = 'Numpad2',
117✔
166
  Num3 = 'Numpad3',
117✔
167
  Num4 = 'Numpad4',
117✔
168
  Num5 = 'Numpad5',
117✔
169
  Num6 = 'Numpad6',
117✔
170
  Num7 = 'Numpad7',
117✔
171
  Num8 = 'Numpad8',
117✔
172
  Num9 = 'Numpad9',
117✔
173

174
  NumAdd = 'NumpadAdd',
117✔
175
  NumpadAdd = 'NumpadAdd',
117✔
176

177
  NumDecimal = 'NumpadDecimal',
117✔
178
  NumpadDecimal = 'NumpadDecimal',
117✔
179

180
  NumDivide = 'NumpadDivide',
117✔
181
  NumpadDivide = 'NumpadDivide',
117✔
182

183
  NumEnter = 'NumpadEnter',
117✔
184
  NumpadEnter = 'NumpadEnter',
117✔
185

186
  NumMultiply = 'NumpadMultiply',
117✔
187
  NumpadMultiply = 'NumpadMultiply',
117✔
188

189
  NumSubtract = 'NumpadSubtract',
117✔
190
  NumpadSubtract = 'NumpadSubtract',
117✔
191
  // NumComma = 'NumpadComma', // not x-browser
192
  // NumpadComma = 'NumpadComma', // not x-browser
193

194
  // Function section https://w3c.github.io/uievents-code/#key-function-section
195
  Esc = 'Escape',
117✔
196
  Escape = 'Escape',
117✔
197
  F1 = 'F1',
117✔
198
  F2 = 'F2',
117✔
199
  F3 = 'F3',
117✔
200
  F4 = 'F4',
117✔
201
  F5 = 'F5',
117✔
202
  F6 = 'F6',
117✔
203
  F7 = 'F7',
117✔
204
  F8 = 'F8',
117✔
205
  F9 = 'F9',
117✔
206
  F10 = 'F10',
117✔
207
  F11 = 'F11',
117✔
208
  F12 = 'F12',
117✔
209
  F13 = 'F13',
117✔
210
  F14 = 'F14',
117✔
211
  F15 = 'F15',
117✔
212
  F16 = 'F16',
117✔
213
  F17 = 'F17',
117✔
214
  F18 = 'F18',
117✔
215
  F19 = 'F19',
117✔
216
  F20 = 'F20',
117✔
217
  PrintScreen = 'PrintScreen',
117✔
218
  ScrollLock = 'ScrollLock',
117✔
219
  Pause = 'Pause',
117✔
220

221
  Unidentified = 'Unidentified'
117✔
222
}
223

224
/**
225
 * Event thrown on a game object for a key event
226
 */
227
export class KeyEvent extends Events.GameEvent<any> {
228
  /**
229
   * @param key  The key responsible for throwing the event
230
   * @param value The key's typed value the browser detected
231
   * @param originalEvent The original keyboard event that Excalibur handled
232
   */
233
  constructor(
234
    public key: Keys,
24✔
235
    public value?: string,
24✔
236
    public originalEvent?: KeyboardEvent
24✔
237
  ) {
238
    super();
24✔
239
  }
240
}
241

242
export interface KeyboardInitOptions {
243
  global?: GlobalEventHandlers;
244
  grabWindowFocus?: boolean;
245
}
246

247
export type KeyEvents = {
248
  press: KeyEvent;
249
  hold: KeyEvent;
250
  release: KeyEvent;
251
};
252

253
export const KeyEvents = {
117✔
254
  Press: 'press',
255
  Hold: 'hold',
256
  Release: 'release'
257
};
258

259
/**
260
 * Provides keyboard support for Excalibur.
261
 */
262
export class Keyboard {
263
  public events = new EventEmitter<KeyEvents>();
1,410✔
264
  private _enabled = true;
1,410✔
265
  /**
266
   * Keys that are currently held down
267
   */
268
  private _keys: Keys[] = [];
1,410✔
269
  /**
270
   * Keys up in the current frame
271
   */
272
  private _keysUp: Keys[] = [];
1,410✔
273
  /**
274
   * Keys down in the current frame
275
   */
276
  private _keysDown: Keys[] = [];
1,410✔
277

278
  public emit<TEventName extends EventKey<KeyEvents>>(eventName: TEventName, event: KeyEvents[TEventName]): void;
279
  public emit(eventName: string, event?: any): void;
280
  public emit<TEventName extends EventKey<KeyEvents> | string>(eventName: TEventName, event?: any): void {
UNCOV
281
    this.events.emit(eventName, event);
×
282
  }
283

284
  public on<TEventName extends EventKey<KeyEvents>>(eventName: TEventName, handler: Handler<KeyEvents[TEventName]>): Subscription;
285
  public on(eventName: string, handler: Handler<unknown>): Subscription;
286
  public on<TEventName extends EventKey<KeyEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
287
    return this.events.on(eventName, handler);
4✔
288
  }
289

290
  public once<TEventName extends EventKey<KeyEvents>>(eventName: TEventName, handler: Handler<KeyEvents[TEventName]>): Subscription;
291
  public once(eventName: string, handler: Handler<unknown>): Subscription;
292
  public once<TEventName extends EventKey<KeyEvents> | string>(eventName: TEventName, handler: Handler<any>): Subscription {
UNCOV
293
    return this.events.once(eventName, handler);
×
294
  }
295

296
  public off<TEventName extends EventKey<KeyEvents>>(eventName: TEventName, handler: Handler<KeyEvents[TEventName]>): void;
297
  public off(eventName: string, handler: Handler<unknown>): void;
298
  public off(eventName: string): void;
299
  public off<TEventName extends EventKey<KeyEvents> | string>(eventName: TEventName, handler?: Handler<any>): void {
UNCOV
300
    this.events.off(eventName, handler);
×
301
  }
302

303
  /**
304
   * Initialize Keyboard event listeners
305
   */
306
  init(keyboardOptions?: KeyboardInitOptions): void {
307
    let { global } = keyboardOptions;
1,410✔
308
    const { grabWindowFocus } = keyboardOptions;
1,410✔
309
    if (!global) {
1,410✔
310
      if (isCrossOriginIframe()) {
1,403!
UNCOV
311
        global = window;
×
312
        // Workaround for iframes like for itch.io or codesandbox
313
        // https://www.reddit.com/r/gamemaker/comments/kfs5cs/keyboard_inputs_no_longer_working_in_html5_game/
314
        // https://forum.gamemaker.io/index.php?threads/solved-keyboard-issue-on-itch-io.87336/
315
        if (grabWindowFocus) {
×
UNCOV
316
          window.focus();
×
317
        }
318

UNCOV
319
        Logger.getInstance().warn('Excalibur might be in a cross-origin iframe, in order to receive keyboard events it must be in focus');
×
320
      } else {
321
        global = window.top;
1,403✔
322
      }
323
    }
324

325
    global.addEventListener('blur', () => {
1,410✔
UNCOV
326
      this._keys.length = 0; // empties array efficiently
×
327
    });
328

329
    // key up is on window because canvas cannot have focus
330
    global.addEventListener('keyup', this._handleKeyUp);
1,410✔
331

332
    // key down is on window because canvas cannot have focus
333
    global.addEventListener('keydown', this._handleKeyDown);
1,410✔
334
  }
335

336
  toggleEnabled(enabled: boolean) {
337
    this._enabled = enabled;
1,855✔
338
  }
339

340
  private _releaseAllKeys = (ev: KeyboardEvent) => {
1,410✔
341
    for (const code of this._keys) {
×
342
      const keyEvent = new KeyEvent(code, ev.key, ev);
×
343
      this.events.emit('up', keyEvent);
×
UNCOV
344
      this.events.emit('release', keyEvent);
×
345
    }
346
    this._keysUp = Array.from(new Set(this._keys.concat(this._keysUp)));
×
UNCOV
347
    this._keys.length = 0;
×
348
  };
349

350
  public clear() {
351
    this._keysDown.length = 0;
36✔
352
    this._keysUp.length = 0;
36✔
353
    this._keys.length = 0;
36✔
354
  }
355

356
  private _handleKeyDown = (ev: KeyboardEvent) => {
1,410✔
357
    if (!this._enabled) {
8!
UNCOV
358
      return;
×
359
    }
360

361
    // handle macos meta key issue
362
    // https://github.com/excaliburjs/Excalibur/issues/2608
363
    if (!ev.metaKey && (this._keys.includes(Keys.MetaLeft) || this._keys.includes(Keys.MetaRight))) {
8!
UNCOV
364
      this._releaseAllKeys(ev);
×
365
    }
366

367
    const code = ev.code as Keys;
8✔
368
    if (this._keys.indexOf(code) === -1) {
8!
369
      this._keys.push(code);
8✔
370
      this._keysDown.push(code);
8✔
371
      const keyEvent = new KeyEvent(code, ev.key, ev);
8✔
372
      this.events.emit('down', keyEvent);
8✔
373
      this.events.emit('press', keyEvent);
8✔
374
    }
375
  };
376

377
  private _handleKeyUp = (ev: KeyboardEvent) => {
1,410✔
378
    if (!this._enabled) {
8!
UNCOV
379
      return;
×
380
    }
381
    const code = ev.code as Keys;
8✔
382
    const key = this._keys.indexOf(code);
8✔
383
    this._keys.splice(key, 1);
8✔
384
    this._keysUp.push(code);
8✔
385
    const keyEvent = new KeyEvent(code, ev.key, ev);
8✔
386

387
    // alias the old api, we may want to deprecate this in the future
388
    this.events.emit('up', keyEvent);
8✔
389
    this.events.emit('release', keyEvent);
8✔
390

391
    // handle macos meta key issue
392
    // https://github.com/excaliburjs/Excalibur/issues/2608
393
    if (ev.key === 'Meta') {
8!
UNCOV
394
      this._releaseAllKeys(ev);
×
395
    }
396
  };
397

398
  public update() {
399
    // Reset keysDown and keysUp after update is complete
400
    this._keysDown.length = 0;
3,529✔
401
    this._keysUp.length = 0;
3,529✔
402

403
    // Emit synthetic "hold" event
404
    for (let i = 0; i < this._keys.length; i++) {
3,529✔
405
      this.events.emit('hold', new KeyEvent(this._keys[i]));
8✔
406
    }
407
  }
408

409
  /**
410
   * Gets list of keys being pressed down
411
   */
412
  public getKeys(): Keys[] {
413
    return this._keys;
1✔
414
  }
415

416
  /**
417
   * Tests if a certain key was just pressed this frame. This is cleared at the end of the update frame.
418
   * @param key Test whether a key was just pressed
419
   */
420
  public wasPressed(key: Keys): boolean {
421
    if (!this._enabled) {
4!
UNCOV
422
      return false;
×
423
    }
424
    return this._keysDown.indexOf(key) > -1;
4✔
425
  }
426

427
  /**
428
   * Tests if a certain key is held down. This is persisted between frames.
429
   * @param key  Test whether a key is held down
430
   */
431
  public isHeld(key: Keys): boolean {
432
    if (!this._enabled) {
6!
UNCOV
433
      return false;
×
434
    }
435
    return this._keys.indexOf(key) > -1;
6✔
436
  }
437

438
  /**
439
   * Tests if a certain key was just released this frame. This is cleared at the end of the update frame.
440
   * @param key  Test whether a key was just released
441
   */
442
  public wasReleased(key: Keys): boolean {
443
    if (!this._enabled) {
7!
UNCOV
444
      return false;
×
445
    }
446
    return this._keysUp.indexOf(key) > -1;
7✔
447
  }
448

449
  /**
450
   * Trigger a manual key event
451
   * @param type
452
   * @param key
453
   * @param character
454
   */
455
  public triggerEvent(type: 'down' | 'up', key: Keys, character?: string) {
456
    if (type === 'down') {
5✔
457
      this._handleKeyDown(
3✔
458
        new KeyboardEvent('keydown', {
459
          code: key,
460
          key: character ?? null
3!
461
        })
462
      );
463
    }
464
    if (type === 'up') {
5✔
465
      this._handleKeyUp(
2✔
466
        new KeyboardEvent('keyup', {
467
          code: key,
468
          key: character ?? null
2!
469
        })
470
      );
471
    }
472
  }
473
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc