43

MDN states that KeyboardEvent.which is deprecated. How can I substitute it for a non-deprecated version?

For example, I have the following:

window.onkeydown = (event) => { console.log(event.which); }

I thought event.key.charCodeAt() could substitute event.which, but this won't work for keys such as ALT, CTRL or ENTER, and it only works if event.key.length === 1:

window.onkeydown = (event) => { console.log(event.key.charCodeAt()); }

To recap, event.which != event.code and event.which != event.key, therefore I am unable to simply use event.key.

Is there a substitute for event.which which detects combination keypresses including ALT, CTRL or ENTER?

Samuel Liew
  • 76,741
  • 107
  • 159
  • 260
flen
  • 1,905
  • 1
  • 21
  • 44
  • 1
    `event.keyCode` and `event.which` are the only 2 parameters that has the ASCII code, it may say that those are deprecated but they are supported on every single browser out there and they probably wont change it – LPZadkiel Mar 14 '18 at 14:08
  • 2
    @LPZadkiel they are not handled uniformly in the different browsers. And since it is deprecated, developers should not use it, since in the future it may not be supported or may not behave as it behaves today. This is exactly why I'm asking if there is a simple non-deprecated version of this, because I find it useful – flen Mar 14 '18 at 14:11
  • 1
    man the recomendation is to use `event.key` but this is browser specific key ergo the recomendation is not using ASCII so is up to you, keep using `which` or `keyCode` or updating to `key` and handle what you need to handle – LPZadkiel Mar 14 '18 at 14:17
  • @LPZadkiel I know this is the recomendation, but this is why I'm asking if there is a better substitute. Maybe someone has some polyfill that is not too verbose, SO is a great place to find such gems. I googled it, but couldn't find anything – flen Mar 14 '18 at 14:19
  • 2
    Related: https://stackoverflow.com/a/39371126 – Samuel Liew May 15 '18 at 00:22
  • 1
    See if this is of any help? https://stackoverflow.com/a/49615856/2830850 – Tarun Lalwani Jul 09 '18 at 06:04
  • Thanks! But I'm not sure a library is needed (and from what I understood, I don't think this library will give me the charCode like `event.which`, so it can't substitute it), I think it's possible to solve this with a long switch statement. If noone gives a better answer, I'll post this later on – flen Jul 09 '18 at 06:49

3 Answers3

51

TL;DR: These are the rules you should follow:

  • When getting text input from the user, use the keypress event along with e.key
  • For shortcuts and other combinations, the built-in way is to use keydown/keyup and check the various modifier keys. If you need to detect chords, you may need to build a state machine.

Background

Keyboard input is split into two phases - keydown/keyup pairs, which track physical keys being pressed, and composed characters that combines multiple sequences of keys to compute a character.

Getting "text"

If you want to know what the operating system thinks the composed sequence is, you should use KeyboardEvent.key

Sample code:

document.getElementById('a').addEventListener('keypress', e => console.log(e.key));
<input id="a" type="text" placeholder="type here">

The reason you want to do this most of the time is because many languages compose characters with several keypresses. The easiest for US-101 keyboards to understand is pressing the shift key + a is A, compared to just pressing a. For languages like Russian, with the altgr dead key, this becomes especially important.

The point I am trying to make is that doing all of this work yourself - detecting key sequences and determining the correct text output is a hard problem™. It is the job of the operating system for a reason.

Now, for older browsers, you may not want to use e.key for lack of older support. Then you can fall back to things like which, or other non-standard approaches.

At some point in the future, keypress may be removed by browsers. The beforeinput event is supposed to be the replacement. However, that event is only supported in chrome, so I'm omitting in here for brevity's sake.

Getting keystrokes

Now then, suppose you are not tracking text, but rather key sequences. This is for things like games, or listening to ctrl-c etc. In this case, the correct thing to do is to listen to keydown/keyup events. For modifier keys, you can simply listen to the ctrlKey, shiftKey, and metaKey properties of the event. See below:

document.getElementById('a').addEventListener('keydown', (e) => {
  const states = {
    alt: e.altKey,
    ctrl: e.ctrlKey,
    meta: e.metaKey,
    shift: e.shiftKey,
  };
  const key = e.key;
  const code = e.code;
  console.log(`keydown key: ${key}, code: ${code}`, states);
});
<input id="a" type="text" placeholder="press ctrl">

As an example, when pressing shift-o on my keyboard, I get the following:

keydown key: Shift, code: ShiftLeft {
  "alt": false,
  "ctrl": false,
  "meta": false,
  "shift": true
}
keydown key: O, code: KeyS {
  "alt": false,
  "ctrl": false,
  "meta": false,
  "shift": true
}

Hopefully the states part is pretty self-evident. They say whether that modifier key was pressed while the other key is down.

The difference between key and code has to do with keyboard layouts. I am using the software dvorak layout. Thus when I type the s key, the scan code that goes to the operating system says s, but then the OS converts that to o because it's dvorak. Code in this case always refers to the scan code (physical key being pressed), while the key corresponds to the operating system's best-effort to figure out what the "text" will be. This isn't always possible, especially with other languages. Again, this is why using the key for the keypress is the right way to go about it.

3rd party libraries

If this doesn't sound particularly easy, that's because it's not. The last time I was looking at this, I came across the mousetrap library, although I'm not sure I would recommend it, given some of the issues I found. It does, however, show an example of building a state machine to track key chords.

Addendum

This is also why you need to track keydown/keyup if you want to eat keystrokes. Since there is no "text" for ctrl+c, you won't get a proper keypress, and thus the browser will natively handle it. If you want to run your own behavior, you need to e.preventDefault() on the keydown itself. (Some of the followup events like copy can also be cancelled, but that's not universally true)

If you also just need to track keys inserted after-the-fact into an input field (or contenteditable div), see the input event.

History: Updated 8/2019 to change keypress->beforeinput

AnilRedshift
  • 7,937
  • 7
  • 35
  • 59
  • Thank you for your elaborate answer! But I'm searching for something that reproduces `event.which` behaviour, which is to give me a "numeric keyCode of the key pressed, or the character code (charCode) for an alphanumeric key pressed." So far, unless I'm missing something, you didn't provide this. But maybe you're right, there is no easy answer, maybe I'll have to write a long switch statement to solve this. I might do this later and post here – flen Jul 08 '18 at 23:57
  • 3
    There's no 1:1 mapping for `which` because `which` tells a lie that it isn't able to keep in non-trivial languages (hence the deprecation). – AnilRedshift Jul 08 '18 at 23:59
  • in the OP's code he's using window.onkeydown why are you example using a `getElementById` and not just `document.body.addEventListener("keydown", (evt) => {})` this would work the same as window.onkeydown then – Barkermn01 Jul 12 '18 at 19:32
  • 3
    Unfortunately, keypress is also deprecated. https://developer.mozilla.org/en-US/docs/Web/API/Document/keypress_event – Gaurang Tandon Jun 20 '19 at 05:25
  • do you know if `e.key === 1` equals `true` no matter which keyboard or OS language is being used?! – oldboy Jul 21 '19 at 06:32
  • @GaurangTandon thanks for the pointer. I added a note about onbeforeinput, but as it's only supported on chrome right now I left it as a minor point. For practical development as of today, keypress is the event to use – AnilRedshift Aug 21 '19 at 05:16
  • @AnilRedshift the use of `keyCode` for me was detecting which range of keys pressed? For example when i need to detect difference of keys on `keydown` is writing word or number or pressing `Home` or `Esc` or `Space` or etc...? by giving a range of code instead of casing one by one. So still it is not an alternative for it. – MMMahdy-PAPION Jun 01 '21 at 08:00
6

As the other answers pointed out, event.which has one main problem: it does not return the same number for different browsers or computers (maybe this is why it is deprecated). Therefore, there is no perfect substitute for it, since it will output different numbers for different users.

So the main problem in trying to create a substitute for it (let's name it: function whichSubstitute(event)) is that the Meta and Shift keys, for example, don't have a unique number that whichSubstitute should get when one of them is pressed, it varies according to OS.

With that in mind, there are two approaches for getting the unicode code point for the user's input.

  1. Getting the unicode value for the character that the user inputted (e.g., ü, which would be 'ü'.codePointAt(0)).
  2. Getting a numeric value for the character that corresponds to the physical key pressed in the keyboard, which might be different from what was inputted to the text field. As AnilRedShift mentioned, the keyboard layout might change the "natural" output from that key in the keyboard, in such a way that the key s might output o. In this case, we'd get 's'.codePointAt(0), instead of getting the value for 'o' (that is, what was actually outputted), like we would get using the first approach. More on this from MDN:

For example, the code returned is "KeyQ" is for the "q" key on a QWERTY layout keyboard, but the same code value also represents the "'" key on Dvorak keyboards and the "a" key on AZERTY keyboards. That makes it impossible to use the value of code to determine name of the key is to users if they're not using an anticipated keyboard layout.

In short: approach number 1 gets the unicode code point for ü, whereas approach number 2 gets the code points for SHIFT, 6 and U (since SHIFT+6+U == ü).

In this answer, we'll use String.prototype.codePointAt() instead of String.prototype.charCodeAt(). The differences are well explained here. The reason is that we can get the whole unicode number with .codePointAt(0), whereas the .charCodeAt(0) would lack .codePointAt(1) to complete the UTF-16 encoded code point.

For approach number 1, we can use the following code:

function whichSubstitute(event) {
  const theKey = event.key;
  if (theKey.length === 1) {
    return theKey.codePointAt(0);
  }
  switch (theKey) {
    case "Backspace":
      return 8;
    case "Tab":
      return 9;
    case "Enter":
      return 13;
    case "Alt":
      return 18;
    case "Escape":
      return 27;
    case "Delete":
      return 127;

    case "Dead": //As of july 2018, Firefox has no support for "Dead" keys https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
      {}
      break;
    case "Unidentified":
      alert("handle the 'Unidentified' if you want to!");
  }

  /*** there are many other possible key values https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
        but, AFAIK, there are no unicode code points for them, such as:

  switch (theKey) {
    case "AltGraph":
    case "CapsLock":
    case "Control":
    case "Fn":
    case "FnLock":
    ...

       event.which may output some number for them, but it is not consistent across
       browsers/machines and they may overlap with other code points. For example:

  case "ArrowUp":
    return 38; //this overlaps with "&"
  case "ArrowLeft":
    return 37; //this overlaps with "%"
  case "ArrowDown":
    return 40; //this overlaps with "("
  case "ArrowRight":
    return 39; //this overlaps with "'"

  ***/

  return 0;
}

//test
document.onkeydown = (event) => {
  console.log('whichSubstitute: ' + whichSubstitute(event) + '; event.which: ' + event.which);
  //note that whichSubstitute gets the ASCII number of 'a', while event.which only gets the ASCII number of 'A' (upper case, always)
}

This, of course, does not solve the problem of getting only one unique consistent number for a pressed key when there is no unicode code point for it (as in the case of Meta). Such keys need to be handled by the programmer according to her/his needs.

For approach number 2, we can use the following code:

function whichSubstitute(event) {
  const theChar = event.code;
  if (theChar.startsWith('Key')) {
    return theChar.codePointAt(3);
  }
  if (theChar.startsWith('Digit')) {
    return theChar.codePointAt(5);
  }

  switch (theChar) {
    case "Backspace":
      return 8;
    case "Tab":
      return 9;
    case "Enter":
      return 13;
    case "Alt":
      return 18;
    case "Escape":
      return 27;
    case "Delete":
      return 127;
    case "Minus":
      return 45;
    case "Plus":
      return 43;
    case "Equal":
      return 61;
    case "Delete":
      return 127;
    case "BracketRight":
      return 93;
    case "BracketLeft":
      return 91;
    case "Backslash":
      return 92;
    case "Slash":
      return 47;
    case "Semicolon":
      return 59;
    case "Colon":
      return 58;
    case "Comma":
      return 44;
    case "Period":
      return 46;
    case "Space":
      return 32;
    case "Quote":
      return 34;
    case "Backquote":
      return 39;

    //there are also "Numpad....." variants

    case "Unidentified":
      alert("handle the 'Unidentified' if you want to!");
  }

  /*** there are many other possible character values https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
        but, AFAIK, there are no unicode code points for them, such as:

  switch (theKey) {
    case "AltLeft":
    case "CapsLock":
    case "ControlRight":
    case "Fn":
    case "NumpadDecimal":
    ...

       event.which may output some number for them, but it is not consistent across
       browsers/machines and they may overlap with other code points.

  ***/

  return 0;
}

//test
document.onkeydown = (event) => {
  console.log('whichSubstitute: ' + whichSubstitute(event) + '; event.which: ' + event.which);
}

This second approach might be useless, since the same physical key might output different unicode characters according to different keyboard layouts. The users might have no idea of which key they should press.

Related: https://www.w3.org/TR/uievents/#keys

flen
  • 1,905
  • 1
  • 21
  • 44
2

From the specification:

which of type unsigned long, readonly

which holds a system- and implementation-dependent numerical code signifying the unmodified identifier associated with the key pressed. In most cases, the value is identical to keyCode

keyCode of type unsigned long, readonly

keyCode holds a system- and implementation-dependent numerical code signifying the unmodified identifier associated with the key pressed. Unlike the KeyboardEvent.key attribute, the set of possible values are not normatively defined in this specification. Typically, these value of the keyCode should represent the decimal codepoint in ASCII [RFC20][US-ASCII] or Windows 1252 [WIN1252], but may be drawn from a different appropriate character set. Implementations that are unable to identify a key use the key value '0'.

See Legacy key models for more details on how to determine the values for keyCode.

(I have omitted the links)

So it is quite easy to create a version that is compatible with the specification. The easiest version just returns 0 for each key.

A slightly more involved version takes event.key and grabs it's ASCII number. You can do the same for the control keys (altKey, ctrlKey, shiftKey).

In short, as it stands the behavior of which is different between systems and browsers. Using the non-deprecated event information you can create a more robust version that removes these differences and will be more future proof.

You can check the behavior of your version with the implementation of which in major browsers. If you are not using any edge cases your version will be both simple and compatible.

Community
  • 1
  • 1
EECOLOR
  • 11,184
  • 3
  • 41
  • 75
  • 1
    Thank you! That's exactly what I thought. It should be easy to implement a workaround, but kind of tiresome. If noone gives a sollution, what I think I'm going to do is to use `event.key` for `event.key.length === 1` and a switch statement for ALT, CTRL and other device control keys, or keys whose length `> 1`. `String.charCodeAt` or `String.codePointAt` can give the number `event.which` would give me – flen Jul 09 '18 at 21:54