jQueryで実装するtapイベント

スマホ対応のWebサイトやアプリで、タップに対してなにか動作させたい時、clickイベントを使うとすこし反応が鈍く感じることがあります。

スマホなどのタッチデバイスでは、clickイベントの発火にわずかなタイムラグが設けられているからのようです(多分ダブルタップを検出するためだと思います)。

そこで、touchstartやtouchendを組み合わせてタップ動作を検出する方法がよく取られますが、より汎用的にjQueryでclickなどと同じように扱えるtapイベントを作りました。

/* jQuery Tap Event */
(function($, window) {
	"use strict";

	var RANGE = 5,
		events = ["click", "touchstart", "touchmove", "touchend"],
		handlers = {
			click: function(e) {
				if(e.target === e.currentTarget)
					e.preventDefault();
			},
			touchstart: function(e) {
				this.jQueryTap.touched = true;
				this.jQueryTap.startX = e.touches[0].pageX;
				this.jQueryTap.startY = e.touches[0].pageY;
			},
			touchmove: function(e) {
				if(!this.jQueryTap.touched) {
					return;
				}

				if(Math.abs(e.touches[0].pageX - this.jQueryTap.startX) > RANGE ||
				   Math.abs(e.touches[0].pageY - this.jQueryTap.startY) > RANGE) {
					this.jQueryTap.touched = false;
				}
			},
			touchend: function(e) {
				if(!this.jQueryTap.touched) {
					return;
				}

				this.jQueryTap.touched = false;
				$.event.dispatch.call(this, $.Event("tap", {
					originalEvent: e,
					target: e.target,
					pageX: e.changedTouches[0].pageX,
					pageY: e.changedTouches[0].pageY
				}));
			}
		};

	$.event.special.tap = "ontouchend" in window? {
		setup: function() {
			var thisObj = this;
			
			if(!this.jQueryTap) {
				Object.defineProperty(this, "jQueryTap", {value: {}});
			}
			$.each(events, function(i, ev) {
				thisObj.addEventListener(ev, handlers[ev], false);
			});
		},
		teardown: function() {
			var thisObj = this;
			
			$.each(events, function(i, ev) {
				thisObj.removeEventListener(ev, handlers[ev], false);
			});
		}
	}: {
		bindType: "click",
		delegateType: "click"
	};

	$.fn.tap = function(data, fn) {
		return arguments.length > 0? this.on("tap", null, data, fn): this.trigger("tap");
	};
})(jQuery, this);

これをtapイベントを使う前にあらかじめ書いておきます。

2016年3月5日追記:タッチデバイスで、clickイベント発生元とtapイベントをバインドした要素が同じ時のみ、クリックイベントのデフォルト動作をキャンセルするよう修正しました。

使い方

$(function() {
	$("#btn").on("tap", function(e) {
		/* なんらかの動作 */
	});
	
	/* ショートカット */
	$("#btn").tap(function(e) {
		/* なんらかの動作 */
	});
});

こんな風にclickイベントなどと同じように使えます。unbindするときも.off("tap")で可能です。

PCなど非タッチデバイスではtapという名のclickイベントとして動くので、別途click時の動作を書く必要はありません。

2016年3月5日追記:非タッチデバイスではclickイベントのデフォルト動作はそのままなので、キャンセルする場合はpreventDefault()などでキャンセルしてください。タッチデバイスではclickイベントのデフォルト動作は自動でキャンセルされます。

簡単な解説

jQueryではjQuery.event.specialにプロパティを追加することでイベントを追加することができます。今回はjQuery.event.special.tapとしてtapイベントを定義しています。

setupにbind時の処理、teardownにunbind時の処理を書きます。

"ontouchend" in windowでタッチに対応しているか判別して、非タッチデバイスの場合は単純にclickイベントのエイリアスになるようにしています。

処理自体はtouchstartでフラグをオンにして、フラグオン時にtouchendが発火したらタップされたと判定しています。ただそれだけだと、スクロールしたい場合もタップされたと誤認識してしまうため、touchmoveである程度指が動いたらフラグをオフにするようにしています。

touchmoveが発火したら問答無用でフラグをオフにする方が簡単ですが、それだと判定がシビアになってしまい使いづらくなります。

余談

jQuery Mobileにはtapイベントが実装されているようです。

Comments

  • ⁂ コロ助 ™

    使いやすくて大変便利です。
    ありがとうございます!

    ただ
    このスクリプトを使って、iosのsafariからフォームを送信しようとすると、
    送信ボタンが動作しませんでした。
    よって、

    $(document).on('tap', ':submit', function(){
    $(this).closest("form").submit();
    });

    の記述もしておいたほうが良いかと思います。

    • ありがとうございます。お役に立てて嬉しいです。

      タッチデバイスではclickのデフォルト動作をキャンセルしているため、仰るとおり、フォームボタンのsubmitはそのままでは機能しません。
      tapという新しいイベントを定義するものなので、その辺はフォーム側のスクリプトで処理する想定です。
      もちろんコメントされているように、すべてのsubmitボタンにtapをつける方法も良いと思います。

      また、非タッチデバイスではclickが発火するので、preventdefaultなどでキャンセルしておかないと、フォームが二度送信されてしまう恐れがあるので注意してください。