KeyboardEvent.keyCodeとは何か

(list)

導入

KeyboardEvent.keyCodeとは何なのかについて少し解説してみたいと思います。

Webアプリでキーボードからの入力を処理する場合に、KeyboardEvent.keyCodeと、 KeyboardEvent.charCodeの二つ(さらにKeyboardEvent.whichを使う人も希に見かけますが)を最初に見かけられた方が多いのではないかと思います。

あなたがこれらの属性を初めて知った時、そのドキュメントはどのように解説していたでしょうか。 KeyboardEvent.charCodeはシンプルで誤解するのも難しいぐらいです。 しかし、KeyboardEvent.keyCodeはそうではありません。 多くの人は、この属性を根本的に間違った情報と共に知ったと思います。

KeyboardEvent.keyCodeの歴史

KeyboardEvent.keyCodeとは何なのかを知るためには、 まずは簡単にその歴史を知っておいた方が良いかと思います。 しかし、残念ながら、私もあまり詳しいことは知りません。 ですので、不確かなことはそのように明示して書きます。

最初にKeyboardEvent.keyCodeを実装したのはInternet Explorerの4もしくはそれ以前と思われます。 とほほのJavaScriptリファレンスによるとNetsacpe Navigator/Communicatior 4.x以前が実装していいなかったかのように記載されています。 念のため、dxr.mozilla.orgで幻のNetscape Communicator 5のソースコードを検索してみても、実装していた痕跡はありません。 mozilla-centralのソースコードの履歴を追いかけてみると、Netscapeが後のNetscape 6の元なるMozillaの ソースコードを公開した当時の履歴は既に残っていませんでした。 ただ、bugzillaを検索してみてもKeyboardEvent.keyCodeどころか、nsIDOMKeyEventを新規に実装するというバグが見つからず、 keyCodeへの言及があるコメントは99年1月とかなので、公開時、つまり98年から実装していたと思われます。 ソースコードを公開した98年当時から実装していました

つまり、IEが遅くとも97年にKeyboardEvent.keyCodeを世に出し、Netscapeがそれを互換性のために98年には実装したという歴史が見えてきます(Netsacpe 6のリリースは2000年と遅くなっていますが)。 ただし、この当時既に存在していた、Operaがどうだったのかは資料が見当たらず分かりません。 WebKitやBlinkの祖先となるKHTMLもこの当時にリリースされているのですがKHTMLにDOMが実装されたのは99年の模様です。 また、Windows以外のInternet Explorerで実装されていたのか、実装状況がどうだったのかに至っては全く分かりません。

Internet ExplroerのkeyCode

歴史を知ると、KeyboardEvent.keyCodeをInternet Explorerが実装し、他のブラウザも追随したということがハッキリします。

ではその提案者であるInternet ExplorerのKeyboardEvent.keyCodeの値がどのようなものなのでしょうか。 これは、当時にGUIアプリをWindowsで開発していた人には簡単ですね。 答えはWindowsのWM_KEYDOWNメッセージ等で使われるvirtual keycodeをそのままセットしていただけなのです。

残念ながら、当時のInternet Explorer 4を実行して確認する環境が手元にはありませんが、現在のInternet Explorerの動作を見ても間違い無いと思います。

そして、この単純な動作こそがKeyboardEvent.keyCodeが酷いものになっていく理由なのでした。

Firefox (Mozilla)のkeyCode

では、MozillaのkeyCode値はどのようなものだったのでしょうか(これには私も戦犯として関わっている話なのですが)。

初期実装時はMozilla (Netscape)に開発者として参加していた訳ではないので推測になりますが、 まずは、ANSIキーボードレイアウト(ノートPCを買おうとした時に選択できることのある英語キーボードのレイアウトです)で全く同じにしようとしたはずです。 これは、当時のWindowsのアプリ開発者なら簡単に分かる話ですので、Windowsでの実装は至って簡単です。 単にOSから渡されるvirtual keycodeをセットするだけで良いのですから。

ここで問題になるのは他のキーボードレイアウトでのkeyCode値と、Windows以外のプラットフォームでのkeyCode値です。 特に後者は大問題で、Windowsがどのようなルールでvirtual keycodeを決めているのか、 特に、記号を入力するキー(WindowsではOEMキーと呼んでいます)のルールが不透明で、 WebKit/BlinkもWindowsとそれ以外ではANSIキーボードレイアウト以外では一部のkeyCode値が異なっています。

話がそれましたので戻しますと、Mozillaは当時、入力される文字からkeyCode値をセットしていました。 この際に、ANSIキーボードレイアウトから入力できる文字、つまり、ASCII文字がそのキーにマップされている場合にのみ、 非Windows環境ではkeyCode値が非ゼロになるという雑な実装を行っていました。

これでは、ASCII capableなキーボードレイアウト以外(例えばギリシャ語、ロシア語、アラビア語、ヘブライ語等)ではほとんどのキーのkeyCode値がゼロになってしまいます。 そこで、Gecko 15の際に、私が、Windows、OS X、Linuxで同じkeyCode値が算出されるように大改修を入れました。 しかし、そのkeyCodeの決定方法を、Mozillaにとって従来通り、 そのキーから入力される文字からマッピングするようにしたのでInternet Explorerとの解離がますます進むことになります。

具体的には、まず、大文字アルファベットを除く全てのASCII文字に対応するkeyCode値を決めました。 次に、押されたキーがASCII文字を入力できる可能性を検査して行きます。 現在のキーボードレイアウトでそのキーを単体で押した場合、 現在のキーボードレイアウトでShiftキーと共にそのキーを単体で押した場合、 これらのうち、先にASCII文字が見つかった場合、その文字に対応するkeyCode値を設定します(数字キーに関してはさらに細かいルールがありますがここでは省きます)。

さらに、Windows以外では、現在のキーボードレイアウトがASCII capableではない場合、つまり、 KeyAからkeyZでASCIIのアルファベットではなく、Unicode文字を入力する場合、 そのユーザがインストールしているASCII capableなキーボードレイアウトでそのキーを単体で押した場合、 次にShiftキーと共にそのキーを押した場合に入力される文字がASCIIのアルファベットかASCIIの数字だった場合、その文字に対応するkeyCode値を設定します。 Windowsではそのような代替キーボードレイアウトの取得が難しい(できなくはない)ので、この処理がWindows以外のみとなっています。 Windowsでは代わりにvirtual keycodeがOEMキーだった場合、単体で入力される文字、 もしくはShiftキーと共に入力される文字がASCIIのアルファベットか数字だった場合に、それに対応するkeyCode値を設定します。

何故ここでASCIIの記号を無視したのかというと、現在のキーボードレイアウトにから入力可能な記号だった場合、 同じkeyCode値が二つ以上のキーにマッピングされるのを避けるためでした。 つまり、KeyboardEvent.keyCodeは現在のキーボードレイアウト内でのキーを一意にする数値という扱いで考えていました。

しかし、これでは、ANSIキーボードレイアウトで記号を入力するキーからUnicode文字しか入力しない、 ロシア語(キリル文字)のキーボードレイアウト等ではOEMキーのkeyCode値が常にゼロとなり、 Firefoxとそのキーボードレイアウトという組み合わせに個別対応しているWebアプリ以外では使い物にならないということで、 Gecko 60からはASCIIの記号に対応するkeyCode値を設定するようになりました。

ChromeのkeyCode値は簡単です。

まず、Windowsでの動作はvirtual keycodeをそのまま設定するというものですので、Internet Explorer/Edgeと同じです。 少なくとも私がテストした範囲では、としか言えませんが。

次に、他のOSでの動作ですが、これが奇妙な結論に至っています。 KeyboardEvent.codeという、新しく標準化されようとしている、キーの物理位置を示す属性がありますが、 基本的にはこれの算出に使うネイティブイベントの値からkeyCode値を決定します。

つまり、Firefoxとは違い、入力される文字とは何の関係もない、ほぼ、 KeyboardEvent.codeを数値にマッピングしたようなものになっています。 ですので、Windowsで物理的なキーの位置を示すscan codeと、 OEMキーのvirtual keycodeがANSIキーボードレイアウトと異なる場合、 他のOSで異なるkeyCode値が計算されてしまうという問題がやはりあります。

標準仕様のkeyCodeとその値とは?

ここまで、各Webブラウザの実装の話をしてきました。 「仕様通りに実装していないブラウザが動作を変えて、統一された値を返すべきなんじゃないの?」 そう思われる方も居ると思います。 その意見はまったくもって正しいです。 ただし、標準仕様が存在していれば、ですが。

W3Cは、2000年11月13日に Document Object Model (DOM) Level 2 Events Specificationという仕様書で、 その様々なDOMイベントの仕様を定義しています。この仕様の、1.6.3. Key eventsの定義は、以下のようになっています。

The DOM Level 2 Event specification does not provide a key event module. An event module designed for use with keyboard input devices will be included in a later version of the DOM specification.

日本語に翻訳してみますと、このDOM Level 2 Event仕様書はkey eventモジュールを提供しません。 将来のDOMの仕様書に、キーボード入力デバイスを利用できるようにデザインされたイベントモジュールが含まれるでしょう。とあります。

では、その、将来のDOM仕様書に当たるものが何かと言いますと、 UI Eventsになります。 こちらは2018年2月現在でも、未だに草案段階です。 つまり、厳密には、未だに勧告されたKeyboardEventの標準仕様は存在していません

ですが、おおむね、各ブラウザベンダ間で合意を得ており、徐々に実装されて来ていますので、 Web開発者の方は、二つ以上のブラウザが実装していれば安心して使える状態になってきていると言えます。

この仕様書のKeyboardEvent.keyCodeの定義によると、

keyCode holds a system- and implementation-dependent numerical code signifying the unmodified identifier associated with the key pressed. Unlike the 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 or Windows 1252, but MAY be drawn from a different appropriate character set. Implementations that are unable to identify a key use the key value 0. See §7.3 Legacy key models for more details on how to determine the values for keyCode.

訳しますと、keyCodeは、モディファイアキー無しで押されたそのキーを示す、システム、もしくは、実装依存のコードです。 key属性とは違い、そのとりうる値はこの仕様では定義しません。 一般的にはkeyCodeの値は、ASCIIかWindows 1252のコードポイントを示す数値であるべきですが、 異なる文字コードを用いて表現されるかもしれません。 キーを識別できない実装の場合、0が用いられます。 どのようにkeyCode値が決定されるのか、より詳細な情報は、7.3節、レガシーキーモジュールを参照してください。とあります。 ブラウザ間、OS間の違いをどうしようもないので、このような文章で定義されていることに注意してください。 ではその、7.3節を見てみましょう。

This section is non-normative

Implementations differ on which values are exposed on these attributes for different event types. An implementation MAY choose to expose both virtual key codes and character codes in the keyCode property (conflated model), or report separate keyCode and charCode properties (split model).

まず、この節は標準仕様ではないとあります。W3Cの標準仕様書で定義できない場合によくある注意書きです。 続いて、実装(つまりブラウザ)は、異なるイベントtypeに対して、これらの属性でどのような値を提供するかが違います。 実装は、virtual keycodeと、文字のコードの両方をkeyCodeプロパティで提供したり(合成モデル)、 もしくは、keyCodeとcharCodeそれぞれのプロパティで提供(分離モデル)してもかまいませんとあります。 そして、続く具体的なkeyCode決定方法として、おそらく架空のモデルが例示されています。

ちなみにこれらの節は、7.2. Legacy KeyboardEvent supplemental interfaceに続く節の内容ですが、 その、7.2節にはこのようにも書かれています。

This section is non-normative

Browser support for keyboards has traditionally relied on three ad-hoc attributes, keyCode, charCode, and UIEvent's which.

All three of these attributes return a numerical code that represents some aspect of the key pressed: keyCode is an index of the key itself. charCode is the ASCII value of the character keys. which is the character value where available and otherwise the key index. The values for these attributes, and the availability of the attribute, is inconsistent across platforms, keyboard languages and layouts, user agents, versions, and even event types.

もちろん、この節は標準仕様ではないから始まり、 ブラウザのキーボードサポートは、3つのアドホック(場当たり的)な属性によって実現されてきました。 keyCode、charCode、そして、UIEventのwhichです。 これら三つ全ての属性は押されたキーを示す数値を返します。 keyCodeはキー自体のインデックスを、 charCodeは文字を入力するキーの文字のASCIIの値を、 whichは文字がある場合はそれを、なければキーのインデックスを。 これらの属性の値、属性が利用できるか否かは、 プラットフォーム間(つまりOS間)、キーボードの言語やレイアウト、 ユーザーエージェント(ブラウザ)やそのバージョン、そしてイベントのtypeによっても統一されていません。とあります。

このように、KeyboardEvent.keyCodeには正式に定義された標準仕様が、今までに一度も定義されておらず、 ブラウザ間やOS間で値が統一されていないということが明記されているぐらいに、 標準仕様の編集者にとっても頭の痛い問題だったのです

それぞれのkeyCodeの意味と、開発者はどうすれば良いのか

上述のように、標準仕様で定義されていない以上、ブラウザの動作こそが全てと言えてしまいます。

各ブラウザのKeyboardEvent.keyCodeの意味をざっくりと定義すると、 Internet Explorer、Edge、Windows版 Google ChromeのものはWindowsのvirtual keycodeドそのまま、 Windows以外のGoogle Chromeのものは物理キーの位置を示すもの、 Firefoxのものはそのキーに関連の深いASCII文字に対応するもの、 と言えます。

あなたはKeyboardEvent.keyCodeの意味をどのようなものだと思っていたでしょうか。 おそらく、多くの人は物理キーの位置を示すものか、もしくは、関係の深いASCII文字に対応するもののどちらかだと思っていた方が多いのではないでしょうか。

何故、ここまでややこしくなったのか、その原因は単純で、当時のMicrosoftが、 マッピングルールが、おそらくは存在しない(キーボードレイアウトのDLLによってマッピングが決められている?)、 Windowsの仮想キーコードをそのままWebアプリに伝えるというナンセンスなことをやってしまったのがそもそもの原因です。

そんなMicrosoftから提案されたのが、このカオスを回避するための新しい仕様、 KeyboardEvent.keyKeyboardEvent.codeの二つです (元々はKeyboardEvent.charというのもありましたが、これはKeyboardEvent.keyに吸収されました)。 しかし、その初期の草案(Internet Explorer 9で実装されいたもの)は、やはり、Windowsというプラットフォームに依存しきっていたので、 危うさを感じた私がいち早くFirefoxで実装し、 W3Cのワーキンググループにクロスプラットフォーム化するための問題点を素早く伝えていったという経緯があったりします。 Microsoftのエンジニアさんを責めるつもりは無いのですが、やはり、 より多くのプラットフォームで実際に実装してみないと正解に近づけないよねってのを再確認させられた事案でした。

というわけで、もし、あなたが新たにWebアプリを作る場合、もう、KeyboardEvent.keyCodeは使わないでください。 既に、SafariもKeyboardEvent.keyKeyboardEvent.codeを2016年のNightlyで実装しているようです (リリース版でどのバージョンになるのかは未確認)。 つまり、既にKeyboardEvent.keyCodeを使う意味というのは、 古いブラウザをサポートする以外の意味はありません。

KeyboardEvent.keyKeyboardEvent.codeは、 KeyboardEvent.keyCodeと違って、数値ではなく、文字列であるというのも互換性の面では非常に有利です。 たとえば、KeyboardEvent.keyCodeが二つのブラウザで異なる値を返す場合、 その値が(255以下の)数値なために、ブラウザを判定した上で特定の数値と比較しなくてはいけません。 それに対して、KeyboardEvent.keyKeyboardEvent.codeは無限にパターン数のある文字列ですので、 実際に、||で接続して、複数の値をブラウザの確認無しに同時に行うことができます。

各ブラウザは自身の過去のバージョンにあわせて作られたWebアプリとの互換性のために、 もはや、keyCodeのマッピングを変更できない袋小路に陥っていますので、 今後もこの非互換問題が解決されることは永遠に無いことを覚えておいてください