KeyboardEvent.keyCodeとは何か
導入
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を検索してみても
ソースコードを公開した98年当時から実装していました。
KeyboardEvent.keyCode
どころか、nsIDOMKeyEvent
を新規に実装するというバグが見つからず、
keyCode
への言及があるコメントは99年1月とかなので、公開時、つまり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
値
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.key
とKeyboardEvent.code
の二つです
(元々はKeyboardEvent.char
というのもありましたが、これはKeyboardEvent.key
に吸収されました)。
しかし、その初期の草案(Internet Explorer 9で実装されいたもの)は、やはり、Windowsというプラットフォームに依存しきっていたので、
危うさを感じた私がいち早くFirefoxで実装し、
W3Cのワーキンググループにクロスプラットフォーム化するための問題点を素早く伝えていったという経緯があったりします。
Microsoftのエンジニアさんを責めるつもりは無いのですが、やはり、
より多くのプラットフォームで実際に実装してみないと正解に近づけないよねってのを再確認させられた事案でした。
というわけで、もし、あなたが新たにWebアプリを作る場合、もう、KeyboardEvent.keyCode
は使わないでください。
既に、SafariもKeyboardEvent.key
とKeyboardEvent.code
を2016年のNightlyで実装しているようです
(リリース版でどのバージョンになるのかは未確認)。
つまり、既にKeyboardEvent.keyCode
を使う意味というのは、
古いブラウザをサポートする以外の意味はありません。
KeyboardEvent.key
とKeyboardEvent.code
は、
KeyboardEvent.keyCode
と違って、数値ではなく、文字列であるというのも互換性の面では非常に有利です。
たとえば、KeyboardEvent.keyCode
が二つのブラウザで異なる値を返す場合、
その値が(255以下の)数値なために、ブラウザを判定した上で特定の数値と比較しなくてはいけません。
それに対して、KeyboardEvent.key
とKeyboardEvent.code
は無限にパターン数のある文字列ですので、
実際に、||
で接続して、複数の値をブラウザの確認無しに同時に行うことができます。
各ブラウザは自身の過去のバージョンにあわせて作られたWebアプリとの互換性のために、
もはや、keyCode
のマッピングを変更できない袋小路に陥っていますので、
今後もこの非互換問題が解決されることは永遠に無いことを覚えておいてください。