Motomichi Works Blog

モトミチワークスブログです。その日学習したことについて書いている日記みたいなものです。

IME確定判定 2021年10月版 (IME確定時に任意の処理を実行する)Compositionイベント

参考にさせていただいたページ

はじめに

以下のタイミングで任意の処理を実行したい。という動機から調査を始めました。

  • IMEを伴わない半角文字入力や文字削除などをしたときは即座に任意の処理を実行
  • IME入力中は何も実行せず、確定のEnterを押したときに任意の処理を実行

古くはkeydownイベントで判定していましたが、User-Agentまで組み合わせた判定が必要となったりするので非常に煩雑で、ブラウザのバージョンアップによってUser-Agentが変化することも少なくないため不具合に繋がりそうです。

一部のブラウザでは、inputイベントが発火したときに渡されるeventオブジェクトにisComposingというプロパティが含まれるようになりましたが、ブラウザによってはisComposingが無かったり、あっても挙動が揃っていなかったりするので使えません。

そこで、input、compositionstart、compositionendの3つのイベントをリスニングして判定します。

サンプルコード

動作を保証するものではありませんが、たぶんこんな感じで判定できると思います。

<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
/* iPhoneでフィールドにフォーカスしたときにズームするのを防止しています。 */
input {
    font-size: 16px ;
}
</style>
</head>
<body>

<input
  id="field"
  type="text"
  name="example"
>
<br><br><br><br><br><br>
<div id="display">window.state.fieldValueはまだ一度も更新されていません。</div>

<script>
window.isComposing = false;
window.state = { fieldValue: '' };
const field = document.querySelector('#field');
const display = document.querySelector('#display');

const handleInput = function (event) {
  console.log('event.type: ', event.type);
  console.log('event.isComposing: ', event.isComposing);

  // `window.isComposing === true`の場合はIME入力途中なので、何もせずreturnします。
  if (window.isComposing === true) return;

  // IMEを伴わない半角入力や文字削除などをしたときに実行されます。
  if (window.isComposing === false) {
    // ここでfieldValueの加工や、stateの更新をします。
    setState(event.target.value);

    // window.state.fieldValueを画面に描画します。
    updateDisplay(window.state.fieldValue);
  }
}

const handleComposition = function (event) {
  console.log('event.type: ', event.type);
  if (event.type === 'compositionstart') window.isComposing = true;
  if (event.type === 'compositionend') window.isComposing = false;

  // `window.isComposing === true`の場合はIME入力途中なので、何もせずreturnします。
  if (window.isComposing === true) return;

  // IME確定したときに実行されます。
  if (event.type === 'compositionend') {
    // ここでfieldValueの加工や、stateの更新をします。
    setState(event.target.value);

    // window.state.fieldValueを画面に描画します。
    updateDisplay(window.state.fieldValue);
  }
}

const setState = function (fieldValue) {
  window.state.fieldValue = fieldValue;
}

const updateDisplay = function (fieldValue) {
  // XSSの脆弱性があるのでこのままの状態で公開しないでください。
  display.innerHTML = fieldValue;
}

field.addEventListener('input', handleInput);
field.addEventListener('compositionstart', handleComposition);
field.addEventListener('compositionend', handleComposition);

</script>
</body>
</html>

動作確認

上記のサンプルコードを作成し、以下のタイミングで任意の処理が実行できることを確認しました。

  • IMEを伴わない半角文字入力や文字削除などをしたときは即座に任意の処理を実行
  • IME入力中は何も実行せず、確定のEnterを押したときに任意の処理を実行

動作確認を行ったOSとブラウザは以下の通りです。

macOS Catalina

Windows 10

iOS 14.7.1 (iPhone 12 mini)

Android

Android 8.1.0 [ColorOS 5.2.1] (OPPO R15 Neo)

Android 7.1.1 (ASUS ZenFone 4 Max: ASUS_X00HD)

以下の2バージョンで動作確認をしました。

IME確定のEnterを押したときのイベントが発火する順番について

以下のように違いがあります。他のブラウザは調査していません。

macOS Chrome

  1. input
  2. compositionend

macOS Firefox

  1. compositionend
  2. input

inputイベントのevent.isComposingについて

上述したように、ブラウザによってinputとcompositionendの発火順が違うため、IME確定のEnterを押したときevent.isComposingの値が異なります。

  • macOSChromeでは、inputが先に発火するため、inputのevent.isComposingはtrueになりました。
  • macOSFirefoxでは、compositionendが先に発火するため、inputのevent.isComposingはfalseになりました。

これらのイベントの発火順からすると当然のことと言えそうです。

おわりに

keydownイベントでやっていた頃と比べたらとても楽になりましたが、もっと楽にきれいに書きやすいように、isComposingが実装されて、イベントの発火する順番が揃ったらいいですねぇ。