Hammer.JSのタップイベントをWii Uでも正しく動くようにする


## 概要

[Hammer.JS]は、タッチジェスチャーに対応するためのJavaScriptライブラリです。
タップやスワイプなど、JavaScriptネイティブの[タッチイベント](http://hakuhin.jp/js/touch_event.html)では実装が面倒なタッチ関連の処理を扱いやすくしてくれます。
また、マウスイベントや(コードを見た限りでは)Pointer Eventにも対応しており、ほぼクロスブラウザな利用ができます。
以下の投稿でも紹介されています。

[新・三大JavaScript タッチ対応ライブラリ比較(Hammer.js/QuoJS/ TouchSwipe) - Qiita](http://qiita.com/icoxfog417/items/b0594efcf46e7eeb2821)

## 問題

しかしこのライブラリ、一部のブラウザでTapを利用する際、1回のタップで2回イベントが発生する事があります。
今のところ、この「一部のブラウザ」に該当するのは、私が確認した範囲ではWii U Internet Browserのみです。
([Hammer.JS]の他のイベントについては、利用する予定がないため検証していません)

Wii Uのブラウザは、ボタンなどをタップすると、「カチッ」という音がしてクリックされます。普通のクリックイベントは、この音がなったタイミングで発生します。
[Hammer.JS]のTapを利用した場合、ボタンなどをタップした瞬間に1回目のイベントが発生し、続いて「カチッ」という音と共に2回目のイベントが発生します。

## 原因

この原因について、Wii Uのブラウザに搭載されているデベロッパーツール[^デベロッパーツール]を使用し検証した所、[Hammer.JS]の内部処理により追加されたタッチイベントが発生した後マウスイベントが反応し、その両方のイベントを[Hammer.JS]がキャッチし、Tapとして処理してしまっている事が原因でした。

この問題を回避するためには、タッチイベントの後に発生するマウスイベントを無視する処理が必要です。

## 対策

原因が分かってしまえば、対策はそれほど困難ではありません。

1回目のタッチイベントでTapの処理を実行しつつ、イベントオブジェクトのtimeStampプロパティからイベントの発生時間を取得し、それを変数で保持します。
2回目のマウスイベントが発生した時は、変数で保持しておいたタッチイベントの開始時間と比較し、タッチイベントの直後300ms未満しか経過していない場合にマウスイベントを無視します。

なお、タッチイベントが300ms未満のタイミングで発生した場合に備え、イベントの種類が異なっているかどうかの判定も必要です。イベントの種類は、イベントオブジェクトのpointerTypeプロパティ[^pointerType]で取得します。

## コード

```js
function addTapEventListener(targetNode, listener) {
    /**
     * @const
     * @type {number}
     * タッチイベントとマウスイベントのタイミング。単位はms(ミリ秒)
     * ここに指定したタイミング未満の間隔で異なる種類のイベントが発生した場合、
     * 2回目以降のイベントを無視する
     */
    var INTERVAL = 300;

    /**
     * @type {number}
     * 直前のイベントの発生時間
     */
    var prevEventTimeStamp = 0;

    /**
     * @type {string}
     * 直前のイベントの種類
     */
    var prevEventPointerType = '';

    /**
     * Hammer.JSに指定するリスナー関数
     * @param {!Object} event Hammer.JSの生成するイベントオブジェクト
     */
    var eventHandle = function (event) {
        /**
         * @type {number}
         * 現在のイベントの発生時間
         */
        var nowEventTimeStamp = event.timeStamp;

        /**
         * @type {string}
         * 現在のイベントの種類
         */
        var nowEventPointerType = event.pointerType;

        /**
         * 以下の条件に合致するか判定
         * 条件に合致しない場合、リスナーの処理を行わず、また直前のイベント用変数にも代入しない
         *
         * + prevEventTimeStampに時間が代入されていない(=初回イベント。直前のイベントが存在しないため、判定処理は無用)
         * + または、直前のイベントと現在のイベントの種類が同じ(本来のTapイベントが短時間に複数回発生する場合の対策)
         * + または、直前のイベントからの経過時間が定数INTERVALで指定した間隔以上
         */
        if (!prevEventTimeStamp || (prevEventPointerType === nowEventPointerType) || (INTERVAL <= (nowEventTimeStamp - prevEventTimeStamp))) {
            /**
             * 現在のイベントの情報を変数に保持
             */
            prevEventTimeStamp = nowEventTimeStamp;
            prevEventPointerType = nowEventPointerType;

            /**
             * リスナーを実行
             */
            listener.call(targetNode, event);
        }
    };

    /**
     * Hammer.JSでTapイベントを設定
     */
    Hammer(targetNode).on('tap', eventHandle);
}
```

```js:使用例
var buttonElem = document.getElementById('button');

addTapEventListener(buttonElem, function () {
    console.log('Tap!');
});
```

[サンプル](http://output.jsbin.com/niziqe)

## 注釈

[^デベロッパーツール]: Wii Uのインターネットブラウザーには、Google Chromeのようなデベロッパーツールが存在します。設定から有効化することで利用できます。詳しくは以下のページをどうぞ:
  [WiiUのインターネットブラウザーの、デベロッパーツールがかなり便利だった - Dr.ウーパのコンピュータ備忘録](http://upa-pc.blogspot.jp/2015/03/wiiu-internet-browser-developer-tool.html)
[^pointerType]: pointerTypeプロパティは、[Hammer.JSのイベントオブジェクト](http://hammerjs.github.io/api/#event-object)に追加される独自のプロパティです。ネイティブのイベントオブジェクトには存在しません。

[Hammer.JS]:http://hammerjs.github.io/