[JavaScript]日本語IME入力中か判定する方法

# 概要
JavaScriptでキーボード押下時などに日本語IME入力中かを判定する方法のまとめです。
日本語入力周りのイベントは、2022年現在主だったブラウザでも挙動に差異があり面倒です。

[IME（全角）入力におけるjsイベント現状調査](https://qiita.com/darai0512/items/fac4f166c23bf2075deb)
こちらの方の記事とだいぶ内容が被っていますが、私の調査結果と微妙に異なるところもあり、改めて自分なりにまとめています。

# 調査対象ブラウザ

- Chrome 99 (Mac)
- Firefox 98 (Mac)
- Safari 15 (Mac)
- Chrome 99 (Windows)
- Firefox 98(Windows)
- Edge 99(Windows)

※ Edgeはバージョン79からChromiumエンジンに変わっており、それ以前は挙動が全く異なると思われるので注意

# 日本語入力時に発生するイベントの流れ

まず、日本語入力時に発生するイベントとその発生順序を確認しました。
OSによる差はなかったので、ブラウザ種別だけで分けて記載しています。

### 日本語IMEが有効な状態で１文字目を入力

Chrome / Edge / Firefoxは`keydown`から始まりますが、Safariは`keydown`で終わるという真逆の挙動になります。

| # | Chrome/Edge       | Firefox           | Safari            |
| --| ----------------- | ----------------- | ----------------- |
| 1 | keydown           | keydown           | compositionstart  |
| 2 | compositionstart  | compositionstart  | compositionupdate |
| 3 | beforeinput       | compositionupdate | beforeinput       |
| 4 | compositionupdate | beforeinput       | input             |
| 5 | input             | input             | keydown           |

### 2文字目を入力

こちらも`keydown`の位置がSafariとそれ以外で真逆です。

| # | Chrome/Edge       | Firefox           | Safari            |
| --| ----------------- | ----------------- | ----------------- |
| 1 | keydown           | keydown           | compositionupdate |
| 2 | beforeinput       | compositionupdate | beforeinput       |
| 3 | compositionupdate | beforeinput       | input             |
| 4 | input             | input             | keydown           |

### EnterでIMEの変換を確定

Safariは２回`input`が発生します。
後述しますが、この2回は1回目のinputTypeが`deleteCompositionText`、2回目のinputTypeが`insertFromComposition`になっており、一度テキストが消えて再挿入されるようなイベントの流れになっています。


| # | Chrome/Edge       | Firefox           | Safari            |
| --| ----------------- | ----------------- | ----------------- |
| 1 | keydown           | keydown           | beforeinput       |
| 2 | beforeinput       | beforeinput       | input             |
| 3 | compositionupdate | compositionend    | beforeinput       |
| 4 | input             | input             | input             |
| 5 | compositionend    |                   | compositionend    |
| 6 |                   |                   | keydown           |


### マウスクリックでIMEを終了

IME変換中に画面などをクリックするとIMEが終了しますが、その際Firefoxだけ`input`が発生します。

| # | Chrome/Edge       | Firefox           | Safari            |
| --| ----------------- | ----------------- | ----------------- |
| 1 | compositionend    | beforeinput       | compositionend    |
| 2 |                   | compositionend    |                   |
| 3 |                   | input             |                   |


# keydownイベントで日本語IME入力中か判定する

keydownイベントはSafari以外では文字入力の一番最初に発生するイベントなので、この時点で判定に使えるのは、keydownイベントで取得できる`KeyboardEvent`オブジェクトだけになります。
具体的には`KeyboardEvent`の以下のプロパティを日本語IME入力の判定に使うことができます。

### KeyboardEvent.isComposing
IMEなどテキスト変換システムが動作中であるかを示すboolean変数です。
この値で判定できれば良いのですが、この値は**Safari以外ではIME入力の一文字目でfalseになります。**

[KeyboardEvent.isComposing](https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/isComposing)

### KeyboardEvent.key
IME入力中は値が`Process`になるという仕様があるのですが、**MacのChromeとSafariはその仕様に対応していませんでした**。

[KeyboardEvent.key](https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/key)
[IME and composition keys](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values#ime_and_composition_keys)

> "Process" [3]: The Process key. Instructs the IME to process the conversion.
> [3] The Process key currently returns "Unidentified" in Firefox and Internet Explorer. Google Chrome returns the value of the key as if IME were not in use.

上記のMDNの説明だとFirefoxは`Unidentified`を返すと書いていますが、実際に確認したら`Process` になっていたので、最近対応されたのかもしれません。

### KeyboardEvent.keyCode

IME入力中は値が`229`になります。
確認した全てのブラウザで共通でしたが、**このプロパティはdeprecated**なのでそのうちなくなる可能性が高いです。


## keydownイベントでの日本語IME判定例

`isComposing`と`key`はブラウザによって値が異なるため、現状、deprecatedの`keyCode`を判定に使わざるをえません。
しかし、`keyCode`が削除されても動くように、`isComposing`と`key`も使って以下のように判定するのが良いんじゃないでしょうか。

```javascript:keydownでの判定イメージ
window.addEventListener('keydown', e => {
  if (e.isComposing || e.key === 'Process' || e.keyCode === 229) {
     // IME入力中
  } else {
     // IME入力中でない
  }
});
```

# beforeinputとinputイベントでIME入力中か判定する
beforeinputとinputイベントでは`InputEvent`オブジェクトが取得でき、InputEventの以下のプロパティが日本語入力の判定に使えます。

### InputEvent.isComposing
この値がtrueならIME入力中と判断できますが、**このプロパティはまだSafariにサポートされていません**。


### InputEvent.inputType
Safari以外のブラウザでは、日本語入力中はどのタイミングでも（変換確定時も）`insertCompositionText`になりました。
しかし、Safariのみ変換確定時の値が異なり、以下のようになります。

#### SafariのinputTypeの値

| #             | イベント      | inputTypeの値        | 
| ------------- | ------------| -----------------     |
| 文字入力       | beforeinut  | insertCompositionText |
|               | input       | insertCompositionText |
| Enterで変換確定 | beforeinut  | deleteCompositionText |
|               | input       | deleteCompositionText |
|               | beforeinut  | insertFromComposition |
|               | input  | insertFromComposition |

Safariも合わせると日本語入力中はinsertCompositionText、deleteCompositionText、insertFromCompositionの３つのinputTypeが発生しましたが、[こちらの資料](https://www.w3.org/TR/input-events-2/)を見ると、`deleteByComposition`という値もあるようです。

> remove a part of the DOM in order to recompose this part using IME

との説明があります。
いまいちどういう状況で発生するものなのかわかりませんが、using IMEとあるのでこの値もIME入力中とみなすことにします。

## beforeinputとinputイベントでの日本語IME判定例

```javascript:keydownでの判定イメージ
window.addEventListener('input', e => {
  const imeTypes = ['insertCompositionText', 'deleteCompositionText', 'insertFromComposition', 'deleteByComposition'];
  if (imeTypes.includes(e.inputType)) {
     // IME入力中
  } else {
     // IME入力中でない
  }
});
```

## compositionstartとcompositionendを使った判定

`beforeinput` は調べた全てのパターンで `compositionstart` と `compositionend`の間に発生するので、`compositionstart` と `compositionend`でフラグ管理してIME入力中かどうかを判定することもできます。

```javascript:beforeinputでの判定イメージ
var isComposing = false;

window.addEventListener('compositionstart', e => {
   isComposing = true;
});

window.addEventListener('compositionend', e => {
   isComposing = false;
});

window.addEventListener('beforeinput', e => {
  if (isComposing) {
     // IME入力中
  } else {
     // IME入力中でない
  }
});
```

ただし、`input`イベントについては、Firefoxのみ変換確定時`compositionend`の後に発生するので同じ方法を使えません。