IME（全角）入力におけるjsイベント現状調査

この記事で対象としているブラウザはChrome(mac/win), Safari(mac), Firefox(mac/win), Edge(win) の最新バージョンで、以降これらをモダンブラウザと表記する。

# tl;dr

- [Safariで全角入力におけるJSイベントの挙動がおかしかったため](#調査背景)調査したところ、仕様(draft含む)上は`InputEvent`だけで判定できそうで期待したが、現状の挙動はベンダー毎に異なった
  - 本命の`inputEvent.isComposing`はSafari/Edgeで対応してなかったりChrome/Firefoxでも挙動が違って使い辛い
  - `inputEvent.inputType`は使える場面がありそう
- `CompositionEvent`はモダンブラウザ全てに実装されており最も有用
  - `input`イベントと組み合わせる場合はFirefoxの`compositionend`イベントの発火順に注意
- `KeyboardEvent.isComposing`はEdge以外のモダンブラウザで実装され、特に`keyup`イベントでの値は各ブラウザで揃っていた

# 仕様

UI EventsのW3C Working Draft（8 November 2018）の入力とイベント発火の表が非常にわかりやすい

- en: https://www.w3.org/TR/uievents/#keys-IME
- ja: https://triple-underscore.github.io/uievents-ja.html#keys-IME

ただ標準仕様に対するベンダーの実装が必ずしも仕様に則しきれていないのは2019年も相変わらず。

# [InputEvent](https://developer.mozilla.org/ja/docs/Web/API/InputEvent)

- 仕様上は[inputType](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/inputType)と[isComposing](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent/isComposing)プロパティでIME入力状態を判別可能
  - ただしisComposingはSafari未実装
  - そしてChrome/Firefoxで挙動が異なる(下記参照)
- inputTypeのENUM値は[Input Events Level 2](https://triple-underscore.github.io/input-events-ja.html#dom-inputevent-inputtype)の仕様(draft)を参考
- [dataプロパティ](https://developer.mozilla.org/ja/docs/Web/API/InputEvent/data)でマルチバイト判定する手もあるが、現状Safari/Chromeのみ

## モダンブラウザの`inputType/isComposing/data`

- 入力に対する`inputType(string)/isComposing(boolean)/data(string)`の値を記載
  - `-`はundefined
- Edge(win)はどちらのプロパティもないため省略

|入力|Chrome|Firefox|Safari|
|---|---|---|---|
|１（全角、未確定）|'insertCompositionText'/true/'１'|'insertCompositionText'/true/-|'insertCompositionText'/-/'１'|
|Enter（上記確定）|'insertCompositionText'/true/'１'|'insertCompositionText'/false/-[^1]|'deleteCompositionText'/-/null|
||||'insertFromComposition'/-/'１'|
|2(半角)|'insertText'/false/'2'|'insertText'/false/-|'insertText'/-/'2'|

- 全角入力が`inputType: 'insertCompositionText'`で半角入力が`inputType: 'insertText'`であることは共通
- Enter押下での確定には一貫性がない
  - compositionendイベント発火前にinputイベントが発火した場合に`isComposing: true`。つまりChromeとFirefoxとでcompositionendの発火タイミングが異なる（後述）
  - safariはinputイベント発火数が他のブラウザより多い
- [マウスで確定させた場合](#入力に対するイベント発火の流れ)は、Chrome/Safariでは確定時にinputイベントは発火しない一方、FirefoxではEnterと同じ値で発火した

# [CompositionEvent](https://developer.mozilla.org/ja/docs/Web/API/CompositionEvent)

IMEによるテキスト編集監視に用いるイベント。半角入力時は当然発火しない。

- [compositionstart](https://developer.mozilla.org/ja/docs/Web/Events/compositionstart): IMEで未確定文字列の入力を開始した時
- [compositionupdate](https://developer.mozilla.org/ja/docs/Web/Events/compositionupdate): IMEで編集中のテキストが変更された時
- [compositionend](https://developer.mozilla.org/ja/docs/Web/Events/compositionend): IME確定時

# KeyboardEvent

キーボード操作、keydown/keyupイベントで監視

- `if (KeyboardEvent.keyCode === 229)`でIME入力を判定しているスクリプトを見かけるが[KeyboardEvent.keyCode](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode)はDeprecated
- Enter押下を検知して力技で......
  - IME確定以外でのEnter押下を区別できるのか疑問
  - ライブ変換とか確定に必要なEnter回数はIMEの設定によって異なる気がする
  - マウスからの確定は当然検知できない
  - とはいえIEサポートしたいならこれしかない？
- `KeyboardEvent.isComposing`の実装はEdge以外は済んでいる
  - このbool値の仕様は`inputEvent.isComposing`同様、compositionstartとcompositionendの間がtrueでそれ以外はfalse
  - しかしSafariだけ下表の通り異なる
  - だがkeyupは揃っており、判定には有益
  - KeyboardEventの発火順は各ブラウザとも[W3Cの仕様](https://triple-underscore.github.io/uievents-ja.html#keys-IME)に即している

# 入力に対するイベント発火の流れ

- 値は`Event.type`
- 太字は`isComposing`がtrue
- Edgeでのマウスクリック確定の方法がわからず未調査

|入力|Chrome|Firefox|Safari|Edge|
|---|---|---|---|---|
|１（全角、未確定）|keydown|keydown|**keydown**|keydown|
||compositionstart|compositionstart|compositionstart|compositionstart|
||compositionupdate|compositionupdate|compositionupdate|compositionupdate|
||**input**|**input**|input|keyup|
||**keyup**|**keyup**|**keyup**|input|
|Enter押下で確定|**keydown**|**keydown**|keydown|compositionend|
||compositionupdate|compositionend|input||
||**input**|input|input||
||compositionend|keyup|compositionend||
||keyup||keyup||
|マウスクリックで確定|compositionend|compositionend|compositionend|?|
|||input[^1]||?|

# その他の手段
## changeイベント

- 入力確定後のEnter押下やfocusが外れた時に発火
- 入力時にリアルタイムに処理したい場合には使えない

# Appendix
## 調査スクリプト

- 単純に各イベントリスナーに`console.log`を入れるだけだとダメ
  - ロギング順と発火順が同じとは限らない
- 各種Eventオブジェクトのtimestampを見てsortして表示

```html:check_inputEvent.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
<body>
<input id="x"/>
<button id="y">out</button>
<script>
var stores = [];
var store = ({timeStamp, type, isComposing, inputType, data, key}) => {
  stores.push({timeStamp, type, isComposing, inputType, data, key});
};
var x = document.getElementById('x');
var y = document.getElementById('y');
x.addEventListener('input', store);
x.addEventListener('keyup', store);
x.addEventListener('keydown', store);
x.addEventListener('compositionstart', store);
x.addEventListener('compositionupdate', store);
x.addEventListener('compositionend', store);
y.addEventListener('click', (e) => {
  console.table(stores.sort((a, b) => a.timeStamp > b.timeStamp));
});
</script>
```

## 調査背景

- 入力値に対してフォーマットした値を再代入するJSを書いた
  - 例えば、郵便番号入力欄で、全角半角問わずリアルタイムに3桁目と4桁目の間にハイフンを補ってあげる処理
- Safariのみ全角入力時に重複入力される現象が起きた
  - 下記は全角１２３４と入力した場合の挙動

![safari_bug.gif](https://qiita-image-store.s3.amazonaws.com/0/100881/e5197e0a-aee4-6f70-68d6-4174f00b41be.gif)

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
<body>
<input id="x"/>
<script>
var formatFn = (v) => v.replace(/^([^-]{3})([^-]{1,})/, '$1-$2');
var inputElement = document.getElementById('x');
inputElement.addEventListener('input', (e) => {
  e.target.value = formatFn(e.target.value);
});
</script>
```

全角入力は確定までフォーマットしない、という方針なら、例えばinputイベント部分を下記のようにすればモダンブラウザ対応可能

```js
let isComposing = false;
inputElement.addEventListener('compositionstart', (e) => {
  isComposing = true;
});
inputElement.addEventListener('input', (e) => {
  if (!isComposing) {
    e.target.value = formatFn(e.target.value);
  }
});
inputElement.addEventListener('compositionend', (e) => {
  e.target.value = formatFn(e.target.value);
  isComposing = false;
});
```

またはinputTypeをみる（Edgeでは常にundefinedだが重複入力問題はSafariのみ）

```js
inputElement.addEventListener('input', (e) => {
  if (e.inputType !== 'insertCompositionText') {
    e.target.value = formatFn(e.target.value);
  }
});
```

### イベントリスナーの確認

何故Safariだけ上記バグがあるのか、Safari（を含むモダンブラウザ）のコンソールでは`getEventListeners(node)`というAPIが使えるので調べて見たが、特に異常はなかった。

<img width="341" alt="safari_api.png" src="https://qiita-image-store.s3.amazonaws.com/0/100881/841855ea-ca83-1c51-dfe6-91674641026b.png">

また`monitorEvents(document)`[^2]で発火イベントを全体的に監視してみた。Safariでは`textInput`イベントが全角確定時の一度だけ発火し、Chromeでは`input`イベントが全角入力の度に発火していた。
（念の為`inputElement.removeEventListener('textInput', listener);`してみたが効果なし）

[^1]: 同じ値
[^2]: https://developers.google.com/web/tools/chrome-devtools/console/command-line-reference?hl=ja#monitoreventsobject-events