日本語IMEで未確定文字が無く且つ変更されたらハンドラを実行するjQueryプラグインを作ってみた

# 2015-08-22 追記

1年半前に書いた記事ですが、この記事の方法は良くないそうなので、参考にしないでください。

twitterでご指摘いただいたのですが、モダンブラウザではkeyup, keydownなどではなく[UI Events Specification (formerly DOM Level 3 Events)](https://w3c.github.io/uievents/)と[Input Events](https://w3c.github.io/editing/input-events.html)の `input` イベントを見て処理すべき、IE8は対応するならIE8だけ別にわけて扱うべきでした。

# 2015-08-23 追記

IE8とIE9では `input` イベントは期待通りに機能しないそうです。
[A near-perfect oninput shim for IE 8 and 9 – Ben Alpert](http://benalpert.com/2013/06/18/a-near-perfect-oninput-shim-for-ie-8-and-9.html) で模擬実装を作っている方がいました。
[Microsoft Support Lifecycle](https://support.microsoft.com/en-us/lifecycle/search?sort=PN&alpha=internet%20explorer)によるとIE8とIE9は約半年後(2016-01-12)には終了となるそうですし、個人的にはそれまでの間にIE8とIE9対応も不要なので、IE8とIE9は非対応にして `input`, `compositionstart`, `compositionend` イベントを使う実装に切り替えました。

ソース: https://github.com/hnakamur/jquery.japanese-input-change
デモ: http://hnakamur.github.io/jquery.japanese-input-change/example.html


# 背景

テキストフィールドでキー入力が落ち着いたらフィルタリングとかをしたいというケースがあります。

英語だけなら、keyupイベントを[jQuery throttle / debounceプラグイン](http://benalman.com/code/projects/jquery-throttle-debounce/docs/files/jquery-ba-throttle-debounce-js.html)のdebounceで処理すればよさそうです。

しかし日本語では簡単ではありません。単にkeyupイベントではIMEで未確定の文字を入力中に反応してしまいます。changeイベントではIEだとテキストフィールドからフォーカスアウトしないと発火されません。

そこで、入力が一段落して且つ値が変わったらハンドラを実行するプラグインを作ってみました。

# デモ

デモ: http://hnakamur.github.io/jquery.japanese-input-change/example.html
デモHTMLソース: https://github.com/hnakamur/jquery.japanese-input-change/blob/v0.3.0/example.html

使い方は、これだけです。

```js
  $("#text1").japaneseInputChange(doSomeWork);
```

また、

```js
  $("body").japaneseInputChange("input[type=text],textarea", doSomeWork);
```

のようにセレクタを指定して呼び出すことも出来ます。プラグインの実装ではjqueryの[.on](http://api.jquery.com/on/)で```.on(selector, handler)```を使っているので、onの実行後に作成された要素にもハンドラが設定されます。

デモ: http://hnakamur.github.io/jquery.japanese-input-change/live-example.html
デモHTMLソース: https://github.com/hnakamur/jquery.japanese-input-change/blob/v0.2.0/live-example.html

## グリッドでフィルタリングするデモ

デモ: http://hnakamur.github.io/slickgrid_example/
解説記事: [jQuery - SlickGridで行をフィルタリングするサンプル - Qiita](http://qiita.com/hnakamur/items/3454bd126ab2e51d5e4f)


使い方は以上で、以下は仕組みの説明です。

# 日本語入力時のキーイベントの調査

調査には[JavaScript : 日本語入力時のキーイベント](http://freefielder.jp/lab/javascript/im.html)を使わせていただきました。ありがとうございます！

試した環境は、OS X Mavericks, 英語キーボード、Google日本語入力、ローマ字入力です。

## Firefoxの場合

最初のkeydownイベントの後、未確定文字がある間はキーを押してもイベントが起きず、Enterで確定するかESCかBackspaceで未確定文字がすべて消された時にkeyupが発生します。

### 「愛」と入力してEnterで確定した場合

```
- keyDOWN : 65
- keyUP : 13(enter)
```

### 「あい」と入力し未確定の状態からESC 1回で取り消した場合

```
- keyDOWN : 65
- keyUP : 27
```

### 「愛」と入力し未確定の状態からESC 2回で取り消した場合

ESC 1回めで「あい」と未変換に戻って、2回めで削除されます。

```
- keyDOWN : 65
- keyUP : 27
```

### 「あい」と入力し未確定の状態からBackspaceを2回押して未確定文字を全て消した場合

```
- keyDOWN : 65
- keyUP : 8(delete)
```

### 「愛」と入力し未確定の状態からBackspaceを3回押して未確定文字を全て消した場合

Backspace 1回めで「あい」と未変換に戻って、2回め以降1文字ずつ削除されます。

```
- keyDOWN : 65
- keyUP : 8(delete)
```

## IE, Chrome, Safariの場合

Firefoxと違い、未確定文字がある状態でもキーが押される度にkeydownとkeyupイベントが発生します。
このとき、keydownのe.whichは229になり、keyupのe.whichは押されたキーになります。

### 「愛」と入力してEnterで確定した場合

```
- keyDOWN : 229
- keyDOWN : 229
- keyUP : 65
- keyUP : 73
- keyDOWN : 229
- keyUP : 32(space)
- keyDOWN : 229
- keyUP : 13(enter)
```

### 「あい」と入力し未確定の状態からESC 1回で取り消した場合

```
- keyDOWN : 229
- keyDOWN : 229
- keyUP : 65
- keyUP : 73
- keyDOWN : 229
- keyUP : 27
```

### 「愛」と入力し未確定の状態からESC 2回で取り消した場合

```
- keyDOWN : 229
- keyDOWN : 229
- keyUP : 65
- keyUP : 73
- keyDOWN : 229
- keyUP : 32(space)
- keyDOWN : 229
- keyUP : 27
- keyDOWN : 229
- keyUP : 27
```

### 「あい」と入力し未確定の状態からBackspaceを2回押して未確定文字を全て消した場合

```
- keyDOWN : 229
- keyDOWN : 229
- keyUP : 65
- keyUP : 73
- keyDOWN : 229
- keyUP : 8(delete)
- keyDOWN : 229
- keyUP : 8(delete)
```

### 「愛」と入力し未確定の状態からBackspaceを3回押して未確定文字を全て消した場合

```
- keyDOWN : 229
- keyDOWN : 229
- keyUP : 65
- keyUP : 73
- keyDOWN : 229
- keyUP : 32(space)
- keyDOWN : 229
- keyUP : 8(delete)
- keyDOWN : 229
- keyUP : 8(delete)
- keyDOWN : 229
- keyUP : 8(delete)
```

# プラグイン実装

GitHubにレポジトリを作りました。
https://github.com/hnakamur/jquery.japanese-input-change

前回記事に書いた時から、以下の変更を行いました。

* セレクタで複数の要素を指定する場合に対応。
* ライブイベント対応。```$(context).japaneseInputChange(selector, handler)```と呼び出した場合は、後から作成された要素にもハンドラを適用。

```js:https://github.com/hnakamur/jquery.japanese-input-change/blob/0.3.0/jquery.japanese-input-change.js
/*! Japanese input change plugin for jQuery.
    https://github.com/hnakamur/jquery.japanese-input-change
    (c) 2014 Hiroaki Nakamura
    MIT License
 */
(function($, undefined) {
  $.fn.japaneseInputChange = function(selector, handler) {
    var readyToCallHandler = true,
      isFirefox = navigator.userAgent.indexOf('Firefox') != -1,
      oldVal,
      callHandler = function(e) {
        var $el = $(e.target), val = $el.val();
        if (val != oldVal) {
          handler.call($el, e);
          oldVal = val;
        }
      };

    if (handler === undefined) {
      handler = selector;
      selector = null;
    }

    return this.on('focus', selector, function(e) {
      oldVal = $(e.target).val();
      readyToCallHandler = true;
    })
    .on('blur', selector, function(e) {
      callHandler(e);
    })
    .on('keyup', selector, function(e) {
      // When Enter is pressed, IME commits text.
      if (e.which == 13 || isFirefox) {
        readyToCallHandler = true;
      }

      // Set timer only when IME does not have uncommitted text.
      if (readyToCallHandler) {
        callHandler(e);
      }
    })
    .on('keydown', selector, function(e) {
      if (isFirefox) {
        // Firefox fires keydown for the first key, does not fire
        // keydown nor keyup event during IME has uncommitted text, 
        // fires keyup when IME commits or deletes all uncommitted text.
        readyToCallHandler = false;
      } else {
        // IE, Chrome and Safari fires events with e.which = 229 for
        // every keydown during IME has uncommitted text.
        // Note:
        // For IE, Chrome and Safari, I cannot detect the moment when
        // you delete all uncommitted text with pressing ESC or Backspace
        // appropriate times, so readyToCallHandler remains false at the moment.
        //
        // However, it is not a problem. Because the text becomes same
        // as oldVal at the moment, we does not invoke handler anyway.
        //
        // Next time key is pressed and if it causes text to change,
        // keydown with e.which != 229 occurs, readyToCallHandler becomes
        // true and handler will be invoked.
        readyToCallHandler = (e.which != 229);
      }
    });
  };
}(jQuery));
```

イベントの発生が一段落してからハンドラを起動する処理については
[\[jQuery\] ウインドウのリサイズ操作が終わった時にだけ処理を実行する | CreativeStyle](http://kadoppe.com/archives/2012/02/jquery-window-resize-event.html)を参考にしました。ありがとうございます！

Firefoxの場合は、IMEで入力し始めた最初のキーについてkeydownが来た後、未確定文字がある間はkeydownもkeyupも発生せず、確定時にkeyupが来るので、keydownでreadyToSetTimerをfalseにして、keyupでtrueにしています。

それ以外のブラウザでは、keydownでe.whichが229ならreadyToSetTimerをfalse, 229以外ならtrueにします。keyupではe.whichが13ならIMEでテキストが確定されるのでtrueに切り替えます。

タイマーから呼ばれる関数では、再度readyToSetTimerをチェックしています。これは一旦アイドルになってタイマーがセットされた後でタイマーが発火する前にまたなにかキーを押し、タイマー発動時に未確定文字がある場合にハンドラを起動しないためです。

さらにハンドラ実行直前にテキストが古いテキストと同じでないことをチェックしています。これは一旦アイドルになってタイマーがセットされた時点でテキストが古いテキストから変わっていたとしても、タイマーが発火する前にキーを押して、ハイマー発動時に古いテキストと同じに戻っている場合にハンドラを起動しないためです。

あと、Firefox以外のブラウザでESCかBackspaceを適切な回数押して未確定文字を全て消し終わったタイミングを判定する方法が思いつかなったので、この時点ではreadyToSetTimerはfalseのままです。が、その時点ではテキストは古いテキストと同じはずなので、ハンドラを起動しないので実害はないです。その後さらにキーを押せばe.whichが229以外でkeydownイベントが来るので、そこでreadyToSetTimerがtrueになります。

未確定の文字がある状態で別のテキストフィールドをマウスクリックしてblurイベントが起きた時に、テキストが古いテキストから変わっていた場合は即座にハンドラを実行するようにしました。

とりあえず、Chrome, Firefox, IE8 (modern.IE on VirtualBox)で軽く試したところでは、希望通りの動きになっているようです。

## 0.3.0での変更点

* delayパラメータを削除して、変更されたら即座にハンドラを実行するようにしました。
 
[日本語IMEで未確定文字が無く且つ文字列が変更された場合に処理を実行するためのBackbone.js用Mixinを作りました - Qiita](http://qiita.com/hnakamur/items/ef911506e71052218fb0)の記事に書いたのですが、[Backgrid.js](http://backgridjs.com/)ではキーイベントで即座にフィルタリング処理を実行していて、フィルタリング処理の方を[underscore.jsのdebounce](https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L710-L745)で間引くようになっています。こちらのほうがあるべき姿だと思いましたので、真似するようにしました。

http://hnakamur.github.io/slickgrid_example/ のデモを試してみた感触は、即座に処理される方が気持ちいいです。

ハンドラの実行が連発されて困る場合は、

* [underscore.jsのdebounce](https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L710-L745)
* [jQuery throttle / debounceプラグイン](http://benalman.com/code/projects/jquery-throttle-debounce/docs/files/jquery-ba-throttle-debounce-js.html)のdebounce

などで処理を間引いてください。