murawaki の雑記

はてなグループから移転してきました

XBL と event model ではまる

indicime が動かないならと、日曜 JavaScripter として自分で書いてみる。するとイベントがらみではまったのでメモ。一応は解決。

やりたいことは indicime と同じ。add-on が window.addEventListener で keypress イベントを登録。捕捉した keypress イベントをもとに、文字を変換した keypress イベントを発生させ、元のイベントはキャンセルする。

コードにすると以下の通り。

// mapped は変換後の文字
var event2 = document.createEvent("KeyboardEvent");
event2.created = true;
event2.initKeyEvent("keypress", true, true, event.view, 
		     false, false, false, false, 
		     0, mapped.charCodeAt(0));
event.target.dispatchEvent(event2);
event.preventDefault();
event.stopPropagation();

結論を先に言うと、event.target を event.originalTarget に変更すると解決した。

症状。ブラウザ上の browser 要素内 (つまり、普通のページの中) でのキー入力は正しく変換できるが、browser 要素外の UI (URL 欄など) では何も起きない。

仕方がないので、両者の振る舞いの違いを JavaScript Debugger (Venkman) で調べてみた。しかし、肝心な部分が native code だから JavaScript から見えない。そもそもイベント処理はあちこちに飛んで分かりにくいのに。それでも 2 点、違いを見つけた。

一つは dispatchEvent の戻り値が違う。browser 要素内のイベントでは false が返る。つまり誰かが preventDefault を呼んでいる。一方 UI 上のイベントでは true が返る。多分、false が返った時は、keypress イベントを受けて、実際の動作に変換している部分があって、そこがイベントをキャンセルしているのだろう。内部実装の都合っぽい気がするが、DOM の Event model 的にこれで良いのだろうか。

もう一つは call stack の変化が違う。browser 内でイベントを発生させると、XPCNativeWrapper function wrapper が積まれて、その上に dispatchEvent が積まれる。さらにその上で listener の handleEvent が次々と呼ばれる。一方、UI 上でイベントを発生させた場合、dispatchEvent を実行した関数の上に、autocomplete.xml の onxblkeypress や tabbrowser.xml の handleEvent が直接積まれる。なぜそうなるのか分からない。ちなみにこれらの XBL の event handler は、control などの特殊なキー入力のみに反応する仕様になっている。

先に書いたように、event.target を event.originalTarget に代えたら両者ともうまくいくようになった。この二つの何が違うのか。例えば URL 欄でイベントを発生させた時、event.originalTarget は html:input だが、event.target は textbox。その親子関係は textbox - xul:hbox - xul:hbox - html:input となっている。event.target.dispatchEvent だと html:input にイベントが届かない。

しかし target と originalTarget の違いをちゃんと理解しているわけではない。MDC に Comparison of Event Targets というページがあるけど説明がない。

nsIDOMNSEvent.idl にも The original target of the event, before any retargetings. とあるだけ。Bug 51263 のおけげで originalTarget が相当昔 (2000年) からあったことが分かったが、作られた経緯は分からないまま。