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

excaliburjs / Excalibur / 14804036802

02 May 2025 09:58PM UTC coverage: 5.927% (-83.4%) from 89.28%
14804036802

Pull #3404

github

web-flow
Merge 5c103d7f8 into 0f2ccaeb2
Pull Request #3404: feat: added Graph module to Math

234 of 8383 branches covered (2.79%)

229 of 246 new or added lines in 1 file covered. (93.09%)

13145 existing lines in 208 files now uncovered.

934 of 15759 relevant lines covered (5.93%)

4.72 hits per line

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

69.62
/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 { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter';
5

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

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

41
  Equal = 'Equal',
1✔
42

43
  IntlBackslash = 'IntlBackslash',
1✔
44
  IntlRo = 'IntlRo',
1✔
45
  IntlYen = 'IntlYen',
1✔
46

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

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

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

126
  Convert = 'Convert',
1✔
127
  KanaMode = 'KanaMode',
1✔
128
  NonConvert = 'NonConvert',
1✔
129

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

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

145
  ArrowUp = 'ArrowUp',
1✔
146
  ArrowDown = 'ArrowDown',
1✔
147
  ArrowLeft = 'ArrowLeft',
1✔
148
  ArrowRight = 'ArrowRight',
1✔
149

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

173
  NumAdd = 'NumpadAdd',
1✔
174
  NumpadAdd = 'NumpadAdd',
1✔
175

176
  NumDecimal = 'NumpadDecimal',
1✔
177
  NumpadDecimal = 'NumpadDecimal',
1✔
178

179
  NumDivide = 'NumpadDivide',
1✔
180
  NumpadDivide = 'NumpadDivide',
1✔
181

182
  NumEnter = 'NumpadEnter',
1✔
183
  NumpadEnter = 'NumpadEnter',
1✔
184

185
  NumMultiply = 'NumpadMultiply',
1✔
186
  NumpadMultiply = 'NumpadMultiply',
1✔
187

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

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

220
  Unidentified = 'Unidentified'
1✔
221
}
222

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

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

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

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

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

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

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

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

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

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

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

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

328
    // key up is on window because canvas cannot have focus
UNCOV
329
    global.addEventListener('keyup', this._handleKeyUp);
×
330

331
    // key down is on window because canvas cannot have focus
UNCOV
332
    global.addEventListener('keydown', this._handleKeyDown);
×
333
  }
334

335
  toggleEnabled(enabled: boolean) {
UNCOV
336
    this._enabled = enabled;
×
337
  }
338

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

349
  public clear() {
UNCOV
350
    this._keysDown.length = 0;
×
UNCOV
351
    this._keysUp.length = 0;
×
UNCOV
352
    this._keys.length = 0;
×
353
  }
354

UNCOV
355
  private _handleKeyDown = (ev: KeyboardEvent) => {
×
UNCOV
356
    if (!this._enabled) {
×
357
      return;
×
358
    }
359

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

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

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

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

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

397
  public update() {
398
    // Reset keysDown and keysUp after update is complete
UNCOV
399
    this._keysDown.length = 0;
×
UNCOV
400
    this._keysUp.length = 0;
×
401

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

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

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

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

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

448
  /**
449
   * Trigger a manual key event
450
   * @param type
451
   * @param key
452
   * @param character
453
   */
454
  public triggerEvent(type: 'down' | 'up', key: Keys, character?: string) {
UNCOV
455
    if (type === 'down') {
×
UNCOV
456
      this._handleKeyDown(
×
457
        new KeyboardEvent('keydown', {
458
          code: key,
459
          key: character ?? null
×
460
        })
461
      );
462
    }
UNCOV
463
    if (type === 'up') {
×
UNCOV
464
      this._handleKeyUp(
×
465
        new KeyboardEvent('keyup', {
466
          code: key,
467
          key: character ?? null
×
468
        })
469
      );
470
    }
471
  }
472
}
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