この日記はMozillaのプロダクトへの貢献者としての私の成果を中心に、気になったバグやWeb界隈の話題について書いていますが、 断り書きがある場合を除き、いかなる団体のオフィシャルな見解ではありません。あくまでも個人的なものです。 Mozilla Foundation、Mozilla Corporation、及び関連企業の公式情報ではないことに注意してください。

現在、XHTML 1.0 (もどき)から、HTML5なコンテンツに修正中です。古い日記は修正が完了していませんので表示が崩れます。 順次、修正していく予定ですのでしばらくお待ちください。

もずはっく日記(2015年1月)

2015年1月1日

KeyboardEvent.keyを利用する前に知っておくべき事 初回投稿日時: 2015年01月01日12時32分46秒
最終更新日時: 2015年01月01日12時33分46秒
カテゴリ: Events Javascript
固定リンク: id=2015010100
SNS: (list)

Firefox (Gecko)では、昨年、D3Eで定義しているKeyboardEvent.keyの実装を大きく前進させました。ですが、この新しい、便利な属性を使う前に色々と知っておいた方が良いことがあります。

まず、KeyboardEvent.keyとはどのようなものなのか、おさらいしておきましょう。これは、過去との互換性から、KeyboardEvent.keyCodeの値を標準化したくてもできないために考案されたものです。KeyboardEvent.keyKeyboardEvent.keyCodeが抱えていた、不明確な(意味不明な)数値という問題を取り除き、コードを読みやすくするため、文字列型になっています。

文字列型だと何が良くなっているのかというと、やはり、一番大きなポイントは、同じ値で別のキーを示すことが少なくなるという点につきます。

例えば、ブラウザAが、64という数字をキーAにマッピングしていたとします。この場合、そのブラウザで、キーAが押されたのか確認するためには、if (event.keyCode == 64) {といった形で検査できます。しかし、ブラウザBが、64という数字をキーBにマッピングしているようなケースが多々あります。このため、先の例は、ブラウザBでは別のキーBが押されたかどうかを検査していることになります。これを、ベタに修正すると、if ((isBrowserA && event.keyCode == 64) || (isBrowserB && event.keyCode == 32)) {という感じになり、非常に扱いづらいです。

ですが、文字列型だと、従来、あるブラウザで使われていた値を回避して、適切な値を新規に簡単に定義できるという利点があります。ですので、ブラウザAが、キーAに"Foo"というキー名を定義し、ブラウザBが、キーAに"Bar"というキー名を定義している場合は、if (event.key == "Foo" || event.key == "Bar") {と単純に検査できますし、文字列という特性上、ブラウザBが、キーBに"Foo"という値を付けている可能性は、本来、ゼロに近いはずなので、ブラウザの検査が不要になってきます(実際には理想通りには行きづらいですが、かなりマシになったのは事実です)。

また、KeyboardEvent.keyが何を意味するのかもおさらいしておきましょう。KeyboardEvent.keyの値は、そのキーが押された状態で、何が起きるべきかを示す値になります。モディファイアキーや、現在選択しているキーボードレイアウト(言語)に依存した値になります。文字を入力するキー(printable key)では、入力される文字がその値となります。それ以外のキーは、事前に仕様で定義されているキー名になります。仕様で定義されていないキー名の場合、"Unidentified"となります。

ちなみに、Mozillaでは、Firefox OSの内部処理等で、仕様で定義されていないキー名がどうしても必要な場合は、"MozFooBar"といった形で、"Moz"プレフィックスを付けていく方針ですので、将来の仕様や、その時点で既に存在しているWebアプリに悪影響を与えることは皆無と思います。この柔軟さが文字列型の偉大なところです。

なお、ゲーム等で、モディファイアキーや、選択されているキーボードレイアウトに関わらず、押されたキーが知りたいという用途もあるでしょう。こちらのために、D3Eでは、KeyboardEvent.codeという属性も定義しているので、興味のある方は仕様書を参照してみてください。

このように、KeyboardEvent.keyは文字列という贅沢な値を利用することで、非常に理想的なものになっているかのように見えます。しかし、現実はそう甘くありませんでした。

実際にKeyboardEvent.keyを利用しようとすると、メンテナンスを行っている時に、すぐに気付く大問題が出てきます。それは、検索のしにくさです。まず、文字入力キー以外のキー名は、単純に、[A-Z][a-zA-Z0-9]+というもので、プリフィックス等がありません。このため、ブラウザが仕様の変更やバグ修正に伴い、キー名を変更してしまうと、そのキー名を利用している箇所を探すのが大変になります。

さらに良くないのは、この簡潔な属性名です。.keyでソースコードを検索してみても、.keyCodeにもマッチしてしまいますので、参照箇所すら簡単にリストアップすることはできません。

私がこの問題に気付いた時には、「時既に遅し」で、定義済みキー名にプリフィックスをつける等の改善は却下されざるをえない状況でした。

ですが、このような欠点があらかじめ分かっていたら、コーディング時に、メンテナンスしやすいコードというのは簡単に書けるはずです。私がお勧めしたいのは、検査したいキー名をグローバル変数に格納してしまうという手法です。本当はconstが使えると良いのですが、IE 10以下に対応するためにはvarを利用するしかありません。

大きなWebアプリの場合、キー名の定義を独立したファイルで行うと、管理がより簡単に済むでしょうから、例えば、d3e-keynames.jsというファイルを作成することをお勧めします。その内容は以下のような感じになるでしょう(ブラウザ判定はいい加減ですので、ちゃんとしたものを調べてください、というか、私よりも実際にWebアプリを書かれている方の方が詳しそう)。

var _isIE = navigator.userAgent.indexOf("MSIE") >= 0;
var _isFx = navigator.userAgent.indexOf("Gecko/") >= 0;
var _isFx25 = isFx && ...;
var _isFx29 = isFx25 && ...;
var _isFx37 = isFx29 && ...;

var KEYNAME_ArrowLeft  = _isIE || (_isFx && !_isFx37) ? "Left" : "ArrowLeft";
var KEYNAME_ArrowRight = _isIE || (_isFx && !_isFx37) ? "Right" : "ArrowRight";
var KEYNAME_ArrowUp    = _isIE || (_isFx && !_isFx37) ? "Up" : "ArrowUp";
var KEYNAME_ArrowDown  = _isIE || (_isFx && !_isFx37) ? "Down" : "ArrowDown";

このように、KEYNAME_というプリフィックスを与えてあげることで、検索が容易になります(この手法だと、キー名自体を検索することは希になると思いますが、KeyboardEvent.keyを利用している箇所を探すには相変わらず必要です)。

また、ブラウザにとって適切な値を予め設定しておくことで、肝心の実装部分が非常にシンプルになります。例えば、上の例のように、最新の仕様案で名前が変化してしまったキー名であっても、if (event.key == KEYNAME_ArrowLeft) {のようにシンプルに検査できます。

KeyboardEvent.keyは、元々、IE 9が実験実装を行って、W3Cに提案したものです。その後、名前の適正化や、ブラッシュアップがW3Cで行われ、Firefox 37以降では、ほぼ、最新の仕様書案に沿った実装になっています。これにより、一部のキー名では例にあるように、既に、非互換が発生しています。

現時点では、Firefox (Gecko)がKeyboardEvent.keyの実装では大きく先行させています。その理由は、オープンソースで実装が誰にでも(他のブラウザ開発者にも)参照できるという形である必要が望ましいという点、もうひとつは、主要なプラットフォームで同時に実装していくので、問題の洗い出しが行いやすい、という点です。

特に、ブラウザ間の互換性を確実なものにするには、前者の要素が不可欠です。また、ブラウザ開発者のみではなく、Web開発者の方向けにも、MDNで、キー名の完全な一覧表を公開しています。他のブラウザ開発者もこの表を参照してくれれば、将来的には完全に同じマッピングが行われたKeyboardEvent.keyが実現するのではないか、という目論見ですし、Web開発者の方は比較的容易にクロスブラウザなスクリプトを書くことができるでしょう。

今後、これを他のブラウザがどのようなスタンスで実装してくるのか、これを観察してみれば、各ブラウザベンダの、Web標準への「意識」がどのようなものなのかを知る、良い機会になるかもしれません。ある意味、ならないことを祈るのみですが。

2015年1月30日

Bug-org 1121878 Fix some missing map between WM_APPCOMMAND and KeyboardEvent.key 初回投稿日時: 2015年01月30日21時33分08秒
カテゴリ: Events Mozilla Core Mozilla37 Mozilla38 Windows バグ修正
固定リンク: id=2015013000
SNS: (list)

Bug-org 865561の修正により、WM_APPCOMMANDをハンドリングする時に、KeyboardEventも発火するようになりましたが、一部、対応が漏れていたので、そのフォーローアップバグです。

無事、Gecko 37にも投入されているため、実際にはこの修正は分からないものになっていると思います。

Bug-org 936313 Drop KeyboardEvent.DOM_LOCATION_MOBILE and KeyboardEvent.DOM_LOCATION_JOYSTICK of KeyboardEvent.location since they have been dropped from D3E spec 初回投稿日時: 2015年01月30日21時40分28秒
カテゴリ: Android Events Firefox OS Gonk Mozilla Core Mozilla38 バグ修正
固定リンク: id=2015013001
SNS: (list)

D3Eの仕様で、以前は、KeyboardEvent.locationの値に、DOM_KEY_LOCATION_MOBILE (4)と、DOM_KEY_LOCATION_JOYSTICK (5)が定義されていましたが、これらがそれぞれ、そもそも必要性がないという点と、GamePad APIの登場で必要無くなったということで、削除されました。

これにあわせ、Geckoもこれらのサポートを終了し、Androidと、Firefox OSでは、KeyboardEvent.codeの値から、他の値のうちのいずれかを割り当てるように修正を行いました。

調査した限りでは、Blinkはこれらの値をそもそも定義していませんし、IE 11は、定義はしているものの、Windows専用ブラウザというポイントを考えると、おそらく、実際には利用していないのではないかと思われます(Windows Phone等では疑わしいものの、不明)。

Bug-org 917322 nsIDOMWindowUtils::SendCompositionEvent() and nsICompositionStringSynthesizer::DispatchEvent() should be able to dispatch events without setting mIsSynthesizedForTests 初回投稿日時: 2015年01月30日21時56分18秒
カテゴリ: Events Firefox OS Mozilla Core Mozilla38 バグ修正
固定リンク: id=2015013002
SNS: (list)

もともと、Geckoでは、テストでの利用目的で、nsIDOMWindowUtils::SendCompositionEvent()や、nsICompositionStringSyntesizerを利用して、IMEの動作をJavascriptからエミュレーションできるようにしていました(要chrome権限)。そして、Firefox OSのJavascriptで実装されているIMEは、Firefox OSが用意しているAPI経由で、これらを利用し、テスト用のイベントを発行しているという状態でした。

Firefox OSのIMEにはGeckoからのリクエストを聞く手段が無いという大きな問題もあり、テストと、Javascript IMEのために、よりしっかりとしたAPIが必要という結論に至りました。

そこで、このバグでは、nsITextInputProcessorというインターフェースを新設しています。これは、インスタンスを生成後に、init()もしくは、initForTests()でDOMウインドウと関連付けてから編集を開始する、というものです。今までのAPIとは異なり、あるインスタンスがすでに編集を開始している場合、他のインスタンスが、既に存在するものと同じトップレベルウインドウに所属しているDOMウインドウでは初期化に失敗するようにしましたので、排他処理も若干、設計に含めています。

また、上述のメソッドで初期化する際に、nsITextInputProcessorCallbackというインターフェースを持つオブジェクトを渡すことで、GeckoからIMEへのリクエストや通知を受け取れるように設計しています。

これらAPIの詳しい情報については、以下の、MDNのドキュメントを参照してください。

内部の設計的には、nsITextInputProcessorはただのJavascript向けのラッパークラスで、実際にイベントの発火や、排他処理、編集のトランザクションを管理しているのは、mozilla::widget::TextEventDispatcherというクラスになります。

将来的には、このクラスをネイティブのIMEイベントハンドラも利用することで、DOMイベント仕様の変更による細かな挙動修正で、各OS向けのコードをメンテナンスするコストを極力下げておき、なおかつ、アドオンが内蔵しているかもしれない、Javascript-IMEとの排他処理を実現できるようにしたいと考えています。