まだ重たいCMSをお使いですか?
毎秒1000リクエスト を捌く超高速CMS「adiary

2015/05/14(木)スマホでドラッグ&ドロップのエミュレーション

jQuery UIdynatree を使用していて、ドラッグアンドドロップ操作が必須であるにも関わらずスマホでは何もできないので、汎用的な実装をjQuery pluginで実現しました。

タッチ操作でDnDをエミュレーションする

タッチパネル系イベントは独特らしく、短くタップしたときのみ mousedown や click 等のマウスイベントが発生してくれますが、長く触っているとマウスイベントは発生しないようです。

ですので、以下のように実装しました。

  • touchstart で mousedown を発火。
  • touchmove で mousemove を発火し、mouseleave と mouseenter をエミュレーション。
  • touchend で mouseup を発火。

mouseover, mouseoutも実装はできますが無視しました。jQuery UIが問題なく動く程度には実装しているつもりです。

エミュレーションがonの状態で短くタップすると、mousedown/mouseupイベントが2重に発生する可能性がありますが、解決策がないので保留です。

ソース

修正BSDライセンスとします。jQuery pluginですので適当に読み込ませて次のように使ってください。

$(dom).dndEmulation();

以下のソースは最新でない可能性があります。最新版は、adiaryのサイトからGitHub経由で「js/adiary.js」を参照して該当部のみ抜粋してください。該当部のみ抜粋する限り修正BSDライセンスで扱って構いません。

var TouchDnDTime  = 700;
$.fn.extend({
//////////////////////////////////////////////////////////////////////////////
// Copyright (C)2015 nabe@abk, New BSD License.
//////////////////////////////////////////////////////////////////////////////
dndEmulation: function(){
	var self = this[0];
	if (!self) return;

	// mouseイベント作成
	function make_mouse_event(name, evt, touch) {
		var e = $.Event(name);
		e.altKey   = evt.altKey;
		e.metaKey  = evt.metaKey;
		e.ctrlKey  = evt.ctrlKey;
		e.shiftKey = evt.shiftKey;
		e.clientX = touch.clientX;
		e.clientY = touch.clientY;
		e.screenX = touch.screenX;
		e.screenY = touch.screenY;
		e.pageX   = touch.pageX;
		e.pageY   = touch.pageY;
		e.which   = 1;
		return e;
	}
	// 自分自身を含めた親要素をすべて取得
	function get_par_elements(dom) {
		var ary  = [];
		while(dom) {
			ary.push( dom );
			if (dom == self) break;
			dom = dom.parentNode;
		}
		return ary;
	}

	// クロージャ変数
	var prev;
	var flag;

	// mousedownエミュレーション
	this.bind('touchstart', function(_evt){
		var evt = _evt.originalEvent;
		prev = evt.target;
		var e = make_mouse_event('mousedown', evt, evt.touches[0]);
		$( prev ).trigger(e);
		
		// ある程度時間が経過しないときは処理を無効化する。
		flag = false;
		setTimeout(function(){
			flag=true;
		}, TouchDnDTime)
	});

	// mouseupエミュレーション
	this.bind('touchend', function(_evt){
		var evt = _evt.originalEvent;
		var e = make_mouse_event('mouseup', evt, evt.changedTouches[0]);
		$( evt.target ).trigger(e);
	});

	// ドラッグエミュレーション
	this.bind('touchmove', function(_evt){
		var evt = _evt.originalEvent;

		// 一定時間立たなければ、処理を開始しない
		if (!flag) return;

		var touch = evt.changedTouches[0];
		var dom   = document.elementFromPoint(touch.clientX, touch.clientY);
		var enter = get_par_elements(dom);

		// マウス移動イベント
		var e = make_mouse_event('mousemove', evt, touch);
		$(enter).trigger(e);

		// 要素移動がなければこれで終了
		evt.preventDefault();
		if (dom == prev) return;

		// 要素移動があれば leave と enter イベント生成
		var leave = get_par_elements(prev);

		// 重複要素を除去
		while(leave.length && enter.length
		   && leave[leave.length -1] == enter[enter.length -1]) {
			leave.pop();
			enter.pop();
		}

		// イベント発火
		var e_leave = make_mouse_event('mouseleave', evt, touch);
		var e_enter = make_mouse_event('mouseenter', evt, touch);
		$(leave).trigger( e_leave );
		$(leave).trigger( e_enter );

		// 新しい要素を保存
		prev=dom;
	});
}
//////////////////////////////////////////////////////////////////////////////
});