jQuery droppable の複数要素が重なった場合の問題

他のブログからエントリーをこちらに移行させて集約します。

今製作中のもので派手にハマッたので、今後バージョン上がった時に対応が必要になるかもしれないのでメモをしておく。
対象箇所は jQuery UI の droppable 系(jquery-1.4.2 + jquery-ui-1.8)。

はじまり

jQuery UI で、指定要素に doroppable を指定してドロップ領域を作成する。ここまでは問題なし。問題なしどころか実装が簡単すぎて涙がでそう。一般的なアプリならここで終わるので特に問題はない。問題となるのは異なるドロップ領域がレイヤーとして重なっている場合。

こちらの想定としては、オレンジ色の部分にドロップしたらオレンジへのドロップ、青い部分にドロップしたら青い部分へのドロップとしたいし、それが普通だと思うんだけど、jQuery.ui.droppable は内部的に複数レイヤーが重なった場合を想定していないようで、交差した黒い部分へドロップすると、オレンジ色へのドロップと認識されてしまう(※オレンジ、青の順に作成した場合)。

理由

3日前に書いた自分のコードでさえ見たくないのに、ましてや jQuery のライブラリとか見たくないんですけどオーラ全開でソースを見てみた訳で、、、結論から言うと確認した範囲で以下の流れで処理されるようだった。

  • droppable 実行
  • 対象の要素情報を内部配列にpush
  • drop イベント発生
  • 内部配列をループさせて drop 位置が要素の領域内であるかを判定
  • 最初にヒットしたものを drop 対象とする

つまり、複数のレイヤが画面上の同じ領域を含んでいて、そこに drop した場合、droppable でドロップ領域化したのが早い方のレイヤに drop が持ってかれるって事みたい。設計時の考慮漏れ?それとも別のやり方があって自分の認識不足?結構調べたけどそれらしい情報は見つけられなかった。

対応策

jQuery UI の 該当箇所を修正する。

custom 版を使っていたのでそれを元に書くけど、修正場所は $.ui.ddmanager だけでよさそう。droppables.default が droppable オブジェクトの格納配列で、対象要素の情報も含まれている。で、さらに修正ポイントを絞ると、$.ui.ddmanager._drop の関数。

drop: function(draggable, event) {

	var dropped = false;
	$.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {

		if(!this.options) return;
		if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance))
			dropped = dropped || this._drop.call(this, event);

		if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
			this.isout = 1; this.isover = 0;
			this._deactivate.call(this, event);
		}

	});
	return dropped;

これを以下の様に修正した。

drop: function(draggable, event) {

	var dropped = false
		 ,max_z = -1
		 ,_that, z, trgt, pos
	;

	$.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {

		if (!this.options) return;
		if ( !this.options.disabled && this.visible ) {

			// get z-index
			trgt = this.element;
			z = ( ( trgt.css('zIndex') == 'auto' ) ? 0 : trgt.css('zIndex') ) || 0;

			// 最大z-indexのものを処理対象にする
			if ( z > max_z ){

				// 対象要素の位置	
				pos = trgt.offset();

				// ドロップ位置に要素が重なっている場合処理
				if ( pos.left <= event.pageX && event.pageX <= parseInt(pos.left) + parseInt(trgt.width())
				  && pos.top  <= event.pageY && event.pageY <= parseInt(pos.top) + parseInt(trgt.height()) ) {
					_that = this;
					max_z = z;
				}

			}

		}

		if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) {
			this.isout = 1; this.isover = 0;
			this._deactivate.call(this, event);
		}

	});

	return (_that) ? _that._drop.call(_that, event) : false;

}

汚いコードだけど。。基本的に ui のコードに直接修正はやらない方が良いので、自分のコードでオーバーライドした方が良いです。修正ポイントとしては、配列に入った要素の中で最大のz-indexを持っていて、drop 場所を要素領域内に含む要素を処理対象としています($.ui.intersect ここら辺とかを上手く使えばもっと良かったのかな)。

まとめ

jQuery とその周辺ライブラリの威力は大きくて、これまで独自に作ってきたものを桁違いのコストで置き換える事が出来るのは周知の事実。でもやっぱりライブラリ。細かい事を突き詰めると結局自分で手を入れる必要が発生する事もある。ライブラリに依存しきってしまってる人はつらいだろうなあ。