近年のウェブアプリケーションでは、テキストボックスに文字を入力して Enter キーを押すことでアクションを起こすようなシーンが増えてきました。フォーム送信ではないため、JavaScript で制御する必要がありますが、いざ自分で実装しようとすると Mac で期待通りに動作しないという問題に直面します。今回は、この原因と対策について解説していきます。
結論だけ見たい方は「解決方法」のセクションからご覧ください。
問題となる現象とは
まずは問題となる現象を確認してみましょう。テキストボックスで Enter キーを押してアクションを起こしたい場合、次のような JavaScirpt コードが思い浮かぶのではないでしょうか。
<input type="text" id="t1">
const input_el = document.getElementById('t1');
input_el.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
window.alert('送信されました。');
}
});
このコードは、テキストボックスで Enter キーを押すと「送信されました。」とアラートウィンドウが表示します。ただし、日本語を入力中に変換の確定のために押した Enter キーには反応しないことを期待しています。次のテキストボックスで日本語を入力してから Enter キーを押して変換を確定してみてください。Windows の Chrome、Edge、Firefox、そして、Mac の Firefox なら期待通りに動作するはずです。
Mac の Safari と Chrome では期待通りに動作しません。日本語の変換の確定のために Enter を押したのに、そのまま送信されてしまいます。なぜそうなってしまうのかを深堀してみましょう。
KeyboardEvent.key の挙動の違い
前述のサンプルコードでは、テキストボックスで keydown イベントが発生したら、KeyboardEvent.key
プロパティ (上記コードでは event.key
) の値を評価しています。この値が "Enter"
なら Enter キーが押されたという意味になるのですが、そもそも、これでなぜ Windows では問題なかったのかが不思議ですよね。
実は、Windows の場合、IME で変換が確定するまでの間、 KeyboardEvent.key
プロパティの値は "Process"
になっています。最後に変換を確定するために Enter キーを押しても、この値は "Process"
です。そして、変換が確定した後に Enter キーが押されると、この値は "Enter"
になります。 したがって、前述のコードは期待通りに動作したわけです。
一方、Mac の Safari や Chrome では、IME (日本語入力プログラム) が ON であろうが OFF であろうが、Enter キーを押したときの KeyboardEvent.key
プロパティの値は常に "Enter
” のままです。そのため、変換の確定のための Enter キー押下がそのまま送信につながってしまうわけです。
KeyboardEvent の isComposing プロパティを使う方法
キーボードイベントを表す KeyboardEvent
オブジェクトには、押されたキーを判定するプロパティがいくつか用意されています。しかし、非推奨となっているプロパティを除いて役に立ちそうなプロパティを探すと、isComposing
プロパティが使えそうです。
このプロパティはまさに IME で変換処理中かどうかを判定するプロパティで、IME が変換を確定する前なら true
を、変換が確定した後なら false
が返ってきます。次のようなコードで動作しそうです。
input_el.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && event.isComposing === false) {
window.alert('送信されました。');
}
});
次のテキストボックスで日本語を入力してから Enter キーを押して変換を確定してみてください。
今度は Mac の Chrome で動作するようになりました。しかし、残念ながら、Mac の Safari では期待通りに動作しません。
IME で変換を確定するために Enter キーを押したときの isComposing
プロパティの値は true
であってほしいのですが、残念ながら、Safari だけは false
になってしまいます。
一見、Safari では isComposing
プロパティがまともに機能していないのではないかと疑いたくなりますが、実はちゃんと機能しています。ただ、Safari だけは、keydown
イベントが発生する前に isComposing
プロパティの値を false
に変更してしまいます。ウェブアプリ開発者にとっては非常に困ったものです。
compositionstart と compositionend イベントを使う方法
input 要素などの編集可能な HTML 要素では、文字入力の際に IME を使うと、変換開始時に compositionstart
イベントが、変換終了時に compositionend
イベントが発生します。これを組み合わせれば、うまく変換確定の Enter キー押下を区別できそうな気がします。
let is_composing = false;
input_el.addEventListener('compositionstart', (event) => {
is_composing = true;
});
input_el.addEventListener('compositionend', (event) => {
is_composing = false;
});
input_el.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && is_composing === false) {
window.alert('送信されました。');
}
});
上記コードは、前述の KeyboardEvent
の isComposing
プロパティを、compositionstart
イベントと compositionend
イベントで代用しています。次のテキストボックスで日本語を入力してから Enter キーを押して変換を確定してみてください。
いかがでしょうか。残念ながら、相変わらず Safari だけうまくいきません。この理由は、イベントの発生順序にあります。
Safari 以外のブラウザーでは、変換確定のために Enter キーを押すと、keydown
イベントが発生た後に compositionend
イベントが発生します。一方、Safari では逆に compositionend
イベントが発生した後に keydown
イベントが発生します。そのため、keydown
イベント発生時に IME 変換状態を知ろうとしても、時すでに遅し、という状況に陥るのです。
前述の isComposing
プロパティのサンプルコードもまったく同じ理由で Safari だけで動作しなかったのです。というのも、isComposing
プロパティの値は、compositionstart
イベントと compositionend
イベントの発生をきっかけに値が書き換わるからです。
そういう意味では、compositionstart
イベントと compositionend
イベントを使ったサンプルコードは、isComposing
プロパティをわざわざ自作したにすぎない、ということになります。
解決方法
以上、私が実際にたどった苦労を紹介したのですが、泥沼に陥っている感じがしますね。しかし解決方法はあります。もちろん、ブラウザーごとに処理を分けるなんて面倒なことはしません。
ただし、最新のウェブ標準仕様に準拠したコードだけを使うという縛りは諦めましょう。そんなことを言ってられる状況ではありません。非推奨のプロパティやイベントであろうが、使えるものは何でも使います。ここでは 3 つの方法を紹介します。
KeyboardEvent の keyCode プロパティを使う方法
非推奨ながらも今回は keyCode
プロパティが役に立ちそうです。keyCode
はキーに割り当てられた数値を返します。IME で変換が終了した後に Enter キーを押すと、keyCode
の値は 13
になります。
input_el.addEventListener('keypress', (event) => {
if (event.keyCode === 13) {
window.alert('送信されました。');
}
});
IME 変換確定中、keyCode
プロパティの値は常に 229
になります。そして IME 変換確定のために Enter キーを押したときも、その値は 229
です。この挙動は Safari を含めてすべてのメジャーブラウザーで同じです。
次のテキストボックスで試してみましょう。Safari でも期待通りに動作します。
HTML5 が話題になる前の Internet Explorer が全盛の頃は keyCode
プロパティは当たり前のように使われていましたので、そう簡単に廃止にはならないと期待したいところです。
keypress イベントを使う方法
これまでウェブ標準に準拠して keydown
イベントを当たり前のように使ってきましたが、代わりに非推奨の keypress
イベントを使うと幸せになれます。
input_el.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
window.alert('送信されました。');
}
});
どうせ非推奨を許容するなら、前述の keyCode
プロパティを使うより、keypress
イベントで代用するほうがスマートかもしれませんね。
次のテキストボックスで試してみましょう。Safari でも期待通りに動作します。
keypress
イベントも、HTML5 が話題になる前の Internet Explorer が全盛の頃から当たり前のように使われていましたので、そう簡単に廃止にはならないと期待したいところです。
form 要素の submit イベントを使う方法
最後にご紹介するのは、意識高いウェブ標準な方でもご納得いただける方法でしょう。非推奨なイベントやプロパティは一切使っていません。
<form>
<input type="text" id="t6">
</form>
const input_el = document.getElementById('t6');
input_el.form.addEventListener('submit', (event) => {
event.preventDefault();
window.alert('送信されました。');
});
どのブラウザーでも、テキスト入力ボックスで Enter キーを押せばフォームを送信する仕組みが標準で備わっています。もちろん、それは Safari でも有効です。このサンプルは、その特性を利用したものです。
input
要素で IME 変換終了後に Enter キーが押されると、親要素である form
要素で submit
イベントが発生します。しかし、それをそのまま受け入れると、フォームを送信しようとしてしまいますので、その挙動を止めるためにイベントオブジェクトの preventDefault()
メソッドを呼び出しています。
次のテキストボックスで試してみましょう。Safari でも期待通りに動作します。
まとめ
個人的には、もし form
要素を追加でマークアップできるのであればウェブ標準に準拠した「form 要素の submit イベントを使う方法」が最もエレガントと思います。そうでなければ、「keypress イベントを使う方法」がシンプルで分かりやすいかもしれませんね。いずれにせよ、Safari だけ挙動が違うというのは勘弁してほしいですね。
今回は以上で終わりです。最後まで読んでくださりありがとうございました。それでは次回の記事までごきげんよう。