【JavaScript】入力フォームで IME 確定時の Enter 誤判定を何とかする

近年のウェブアプリケーションでは、テキストボックスに文字を入力して 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('送信されました。');
    }
});

上記コードは、前述の KeyboardEventisComposing プロパティを、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 だけ挙動が違うというのは勘弁してほしいですね。

今回は以上で終わりです。最後まで読んでくださりありがとうございました。それでは次回の記事までごきげんよう。

Share