参考にさせていただいたページ
はじめに
以下のタイミングで任意の処理を実行したい。という動機から調査を始めました。
古くは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>
動作確認
上記のサンプルコードを作成し、以下のタイミングで任意の処理が実行できることを確認しました。
動作確認を行ったOSとブラウザは以下の通りです。
macOS Catalina
Windows 10
iOS 14.7.1 (iPhone 12 mini)
Android
Android 8.1.0 [ColorOS 5.2.1] (OPPO R15 Neo)
- Chrome 94.0
Android 7.1.1 (ASUS ZenFone 4 Max: ASUS_X00HD)
以下の2バージョンで動作確認をしました。
IME確定のEnterを押したときのイベントが発火する順番について
以下のように違いがあります。他のブラウザは調査していません。
macOS Chrome
- input
- compositionend
macOS Firefox
- compositionend
- input
inputイベントのevent.isComposingについて
上述したように、ブラウザによってinputとcompositionendの発火順が違うため、IME確定のEnterを押したときevent.isComposingの値が異なります。
- macOSのChromeでは、inputが先に発火するため、inputのevent.isComposingはtrueになりました。
- macOSのFirefoxでは、compositionendが先に発火するため、inputのevent.isComposingはfalseになりました。
これらのイベントの発火順からすると当然のことと言えそうです。
おわりに
keydownイベントでやっていた頃と比べたらとても楽になりましたが、もっと楽にきれいに書きやすいように、isComposingが実装されて、イベントの発火する順番が揃ったらいいですねぇ。