ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

HTML / DOM におけるキーボードイベント周りの話

最近キーボードショートカットの実装をしようと思ってキー入力によるイベント周りについて調べてみたのだけれど、日本語でまとまった情報が見つからなかったので、キーボードショートカットの実装に必要そうな内容を簡単にまとめておこうと思う。 キーボードショートカットに限らず、キー入力によるイベント周りの何かをする場合には参考になると思う。

本記事では、DOM 3 Events spec の 2012-09-06 の版を参照しており、将来の版では変更されている可能性がある。 最新の版は下記リンクから確認のこと。

keydown イベント、keypress イベント、keyup イベントについて

  • keydown イベント は、キーが押されたときに発生するイベント
    • 押されたキーの種類によってデフォルトアクションが異なる
    • keydown イベントのデフォルトアクションのキャンセルは keypress イベントの発生を阻害するが、keyup イベントの発生は阻害しない (参考: キーボードイベントのキャンセル)
  • keypress イベント は、文字値 (character value) を生成するのが通常の動作であるようなキーが押されたときに発生するイベント
    • keypress イベントが発生するのは keydown イベントのデフォルトアクションとして
    • DOM 3 Events によると keypress イベントは Deprecated なので、使わなくて済むなら使うべきではない
      • 例えば、テキストエリアなどへのキー入力の検出には input イベント を使うべき
  • keyup イベント は、キーが離されたときに発生するイベント

KeyboardEvent インターフェイス

keydown イベント、keypress イベント、keyup イベントに対するイベントリスナには KeyboardEvent インターフェイス を実装したオブジェクトが渡されてくる。 このオブジェクトは、どのキーが押されたかを特定するための key 属性と char 属性を持っている。

KeyboardEvent#char 属性

KeyboardEvent#char 属性 には、押されたキーの文字値 (character value) が入っている。 もし、キー押下が表示可能な表現 (printed representation) を持っている場合は、この属性は空文字列ではないユニコード文字列になる *1。 押されたキーがマクロで複数文字を挿入するようなものである場合など、この属性は複数文字からなる文字列になり得る。

押下されたキーが表示可能な文字列表現を持たない場合は、空文字列。

KeyboardEvent#key 属性

KeyboardEvent#key 属性 には、押されたキーのキー値 (key value) が入っている。 その値が表示可能な表現を持つ場合は、key 属性の値は char 属性に適合する (match) 値 *2 になり、そうでない場合は key values set で定義されたキー値のうちの 1 つになる。

実践的な話

そもそも char 属性と key 属性は、広く使われているブラウザの中ではまだ IE と Opera にしか実装されていない (IE 10 と Opera 12.14 で確認)。 Firefox は実装作業中っぽい (680830, Bugzilla)。 なので、多くのブラウザをサポートする必要がある場合は、後述する charCode や keyCode を併用する必要がある。

char 属性と key 属性を使う場合、例えば Enter キーが入力されたかどうかを調べるには、以下のように key 属性の値が "Enter" であり、char 属性の値が key 属性の値と異なることをチェックするのが良さそう。 (char 属性が key 属性と異なることを調べるのは、例えばマクロで "Enter" という文字列を入力するキーが存在する場合にも evt.key は "Enter" になる *3 ため。)

elem.addEventListener("keydown", function (evt) {
    // Enter キーが押下された?
    if (evt.key === "Enter" && evt.key !== evt.char) {
        // Enter キーが押下された!!
    }
}, false);

文字を表すキーが入力されたかどうかを調べる場合は char 属性だけを見れば良さそう。

レガシーな KeyboardEvent のための補助インターフェイス

char 属性や key 属性が KeyboardEvent でサポートされていない実装の場合、代わりに charCode 属性や keyCode 属性を使う必要がある。 それらは、レガシーな KeyboardEvent のための補助インターフェイスとして DOM 3 Events で定義されている。

charCode 属性

文字入力を引き起こす keypress イベントを補足したときの Event オブジェクトの charCode 属性 は、文字値 (character value) を保持する。 その値は、入力文字の Unicode コードポイントである。 keydown や keyup イベントでは、値は 0 になる。

keyCode 属性

keyCode 属性 は、システム依存かつ実装依存の、押下されたキーの識別子を示す無修飾 (unmodified) *4 の数値コード (numerical code) を保持する。 詳細については次のセクションに書かれている。

システム依存かつ実装依存ではあるのだけれど、IME (Input Method Editor) の処理が動いていて、かつイベントの種類が keydown の場合には 229 になるとか、数字の入力の際はその ASCII コードが返るとか、Fixed virtual key codes の表にあるキー (Enter キーとか矢印キーとか Detele キーとか、基本的な制御キーが一通り含まれている) についてはそれが使われるとか、複数のシステム、実装でも同様に扱える部分も結構ある。

ちなみに keypress イベントについては、charCode 属性にも keyCode 属性にも同じ値が入っている実装と、charCode 属性にのみ値が入っていて keyCode 属性は常に 0 であるような実装の 2 種類がある (Firefox 19 は前者、IE 10 は後者だった)、ということに DOM 3 Events 上ではなっている。 しかし他にも、charCode 属性がそもそもなくて、仕様上は charCode 属性に入っているべき値が keyCode 属性に入っている実装もある (IE 6, 8 で確認。 keypress イベントでも charCode 属性は undefined で、charCode に入っているべき値が keyCode に入っていた。)。

実践的な話

まず、keyCode 属性と charCode 属性の使い分けだが、以下のようにするのが正しそう。

  • keydown イベントと keyup イベントでは keyCode 属性をみる
  • keypress イベントでは charCode 属性をみて、undefined の場合は keyCode を使う

また、どのようにキーボードイベントを処理するのかにもよるが、基本的には以下のようにするのが良さそう。

  • Enter キーや Esc キー、矢印キーなどの制御キーは keydown イベントで捕捉して処理する
  • アルファベットや数字キー、その他記号などの文字入力は keypress イベントで捕捉して処理する

もちろん、key 属性や char 属性が使える場合については、全て keydown イベントを捕捉して処理すべきである。

イベントの発生順序とか繰り返しの話

keydown イベントと keypress イベントは順番に発生する。 キーを押し続けている場合、keydown イベントと keypress イベント *5 が繰り返し発生する。 その間隔はもちろんシステム依存。 キーを離すと keyup イベントが発生する。

とりあえず最近のブラウザは大体これにあった実装になってるようだ *6

DOM 3 Events には、繰り返しの場合はイベントオブジェクトの repeat 属性が true になるって書いてあるけど、実装されているブラウザはあまりなさそう。 IE 10, Opera 12.14 だと repeat 属性は常に false っぽい。 Firefox 19 と Chrome 25 だと repeat 属性は実装されてない。 IE 6, IE 8 ではちゃんと実装されてた。 なんで古い IE で実装されてて IE 10 で動いてないんだろ...。

実践的には、keyup イベントが発生するまではそのキーが押され続けている、というような判断方法をするのが良さそう。

Input Method Editor に関するイベント

Input Method Editor (IME) などの text composition system によるテキスト編集中でも、キーが押下されると keydown イベントが発生する *7。 しかしながら、text composition system が動いているときはキー押下によるイベントをスルーしたいという場面は往々にあると思う。 例えば、テキスト入力欄に何かしらのキーボードショートカットを付けている場合 (Enter キーで送信とか) などがそうである。

IME のセッション開始時 (テキスト編集開始時) には compositionstart イベントが、セッション終了時には compositionend イベントが発生する。 文字変更時の 詳細は下記ページ参照。

別の方法

text composition system が動いているときには、keydown イベントで渡されるイベントの keyCode は 229 にするように書かれている ので、CompositionEvent がサポートされていない環境でも、keyCode を見て 229 なら text composition system が動いている判断することも可能である。 (Opera 12.14 だと CompositionEvent がサポートされていないので、keyCode を見る方法が良さそう。)

あとは、text composition system が動いている場合には keypress イベントが発生しない環境が多いと思うので、keydown イベントで制御キーを受け取って、keypress イベントで実際に動かす、というようなことをすればよい。 と思うが、そもそも keydown イベントで keyCode を見る場合は、IDE が動いていると keyCode は 229 になるのでそんなに気を使う必要はなさそう。

気を付けるべきは key 属性や char 属性を使う場合のみ、かな。

最終的に

キーボードショートカットを実装するなら以下のような流れでイベント処理をするのが良さそう。

  • keydown イベントを検知して
    • evt.key, evt.char が実装されている場合 evt.char を使って処理
    • そうでない場合、evt.keyCode を見て、
      • Fixed virtual key codes に含まれているものならそれを使う
      • さもなければ keypress イベントに任せる
  • keypresss イベントで String.fromCharCode(evt.charCode || evt.keyCode) を見て、対象となる文字ならなんかする

あとは、テキスト入力欄にキーボードショートカットをつける場合で evt.key や evt.char を使う場合に関しては、text composition system の編集のセッション中かどうかを気にすべし、ってところか。

補足

IE 10 だと evt.key や evt.char を便利に使えるわけなので、Windows ストアアプリでショートカットキーを実装するときにはそれらの属性を使うといいと思います。 Web 上だと、やはりまだサポートしているブラウザが少ないので、今のところはレガシーな API だけを使うようにした方が条件分岐が少なくて楽かもしれないですね。

追記: ゲームや RIA のショートカットのためには DOM 3 Events の API でも不足しているという話

今回私が実装したいと思っていたショートカットは、例えばテキスト入力欄 (textarea 要素) で Ctrl + Enter を押したときに投稿する、というようなもので、ショートカットが使えなかったら困るというものではなかった。 なので、既存の API でもまあなんとかなるかなぁというものなのだったわけだが、『ゲームや Rich Internet Application (RIA) で使うショートカットのための機能としては DOM 3 Events の API では不足している』、という話を見つけたので紹介しておく。

このブログ記事での主張は、要約すると以下のようなものである。

ショートカットキーをキーの文字で指定してしまうと、そのキーを持っていないキーボードでは使えない、というような問題がある。 そこで、様々なキー配列に対応するために一般的にはユーザーがキーボードショートカットを設定できるようにする。 そのとき必要となるのは以下のものである。

  • ユーザーにキーの説明をする際に表示できるキーの説明 (Shift などを同時に押していても変化しない、そのキーのプライマリーな表示か、そのキーの機能)
  • Shift などを同時に押していても変化しない、キー固有の識別子

残念ながら DOM 3 Events の API はこれらを満たしていない。

上記記事の筆者は W3C の ML にこれに関する投稿を行っており、その議論の中で、UI Events として新たな API の仕様策定が進んでいることを知らされた模様。

検証用ページ

*1:algorithm for determining the key value に従うようにして決められる。

*2:char 属性が空文字列でない場合、key 属性は char 属性と同じである場合もあるが、必ずしもそうではない。

*3:はずだと思っているが違うかもしれない

*4:Shift キーや Alt キーが押されていたとしても変化しないってことだと思う。

*5:keypress イベントが発生しないようなキー押下の場合は keydown イベントのみ

*6:IE 10, 8, 6, Firefox 19, Opera 12.14, Chrome 25 で確認

*7:これも実装依存で、IE 10 だとそのようになったけど Firefox 19 では違った