drag.on - ドラッグイベントの内容を設定する

投稿日: / 更新日:

d3.jsのdrag.onは、ドラッグ操作に対応したイベントリスナーを設定するためのメソッドです。イベントリスナーが受け取る情報に応じて、要素を動かすための処理を設定しないといけません。

サンプルコード

<div>ドラッグ対象の要素</div>
// ドラッグ操作のビヘイビアを代入
var drag = d3.behavior.drag() ;

// ドラッグ操作に対応するイベントリスナーを指定する
drag.on( "drag", function() {
	// イベントオブジェクト
	var event = d3.event ;

	// X座標とY座標を取得
	var x = event.x ;
	var y = event.y ;

	// 前回発生時から今回発生時の間に動いた距離
	var dx = event.dx ;
	var dy = event.dy ;
} ) ;

// div要素に対して、call()メソッドでドラッグ操作を適用
select( "div" ).call( drag ) ;

デモ

構文

instance = drag.on( type [, listener ] )

引数

項目説明
typeイベントハンドラを指定する。dragstart(ドラッグの開始)、dragend(ドラッグの終了)、drag(ドラッグ移動中)の3種類から指定可。
listener省略可。設定したいイベントの関数を指定する。nullを指定した場合、既に設定されているイベントが削除される。第2引数以降を省略した場合、要素に設定されているイベントを取得する。

戻り値

項目説明
dragインスタンスが戻り値となる。

チュートリアル

drag.onを利用してイベントリスナーを設定しても、対象の要素がドラッグ操作に合わせて動くわけではありません。あくまでも、その要素上でドラッグ操作をした時の位置座標などの情報を受け取れるようになるだけに過ぎません。そのため、受け取った情報に応じた処理は自力で設定しなければいけないわけです。というわけで、追加情報としてこの章では、ドラッグ操作で要素を動かせるようになるまでを説明します。

インスタンスの作成

まずは、要素にドラッグ操作を適用しましょう。ここまでは決まった手順なので、特に考えなくても大丈夫だと思います。

<div id="parent">
	<div id="target">対象の要素</div>
</div>
// ドラッグ操作のビヘイビアを代入
var drag = d3.behavior.drag() ;

// 要素にドラッグ操作を適用する
d3.select( "#target" ).call( drag ) ;

ドラッグイベントの設定

ここからは、対象となる要素上でドラッグ操作が起こった場合に、その操作に応じて処理をするためのイベントリスナーの内容を設定していきましょう。drag.onでは3種類のイベントハンドラを指定できます。確認しておきましょう。

イベントハンドラの種類
項目説明
dragstartドラッグ操作の開始時に発火する。
dragドラッグ操作中に繰り返し発火する。
dragendドラッグ操作の終了時に発火する。

dragstartとdragend

ということで、まずは簡単なdragstartと、dragendに応じたイベントを設定してみましょう。ドラッグを開始した時に要素を赤くして、終了した時に元の色に戻すという内容にします。こうすることで「ドラッグ操作中だけ赤になる」という挙動になります。もちろん、この時点では要素はピクリとも動きません。

// 開始時のイベント
drag.on( "dragstart", function() {
	// this=要素 (<div id="target">対象の要素</div>)

	// 背景を赤にする
	this.style.background = "red" ;
} ) ;

// 終了時のイベント
drag.on( "dragend", function() {
	// this=要素 (<div id="target">対象の要素</div>)

	// 背景を茶色にする
	this.style.background = "#D36015" ;
} ) ;

このコードを確認する

drag

いよいよ、肝となるdrag(ドラッグ操作)のイベントリスナーを設定しましょう。このハンドラだけ、関数内のイベントオブジェクト(d3.event)に下記の4つのプロパティが含まれます。dragのイベントは、ドラッグ操作中に何十回、何百回と起こるイベントです。マウス、またはタッチをわずかに動かす度に起こるイベントです。これを意識して下さい。xyでドキュメント上の絶対的な位置座標、dxdyでは前回を対象にした相対的な位置座標を参照できます。

参照できる情報
項目説明
dx前回のX座標と今回のX座標の差分。
dy前回のY座標と今回のY座標の差分。
xドキュメント上のX座標。
yドキュメント上のY座標。

やり方はたくさんありますが、今回は動かすdiv要素にスタイルシートでposition: relativeを指定して、topleftの値を動的に変えていくことにしましょう。どういう風に動かすかというと、X方向に動いた距離の分だけleftに、Y方向に動いた距離の分だけtopに加算していけばいいんです。動いた距離はdxdyで参照できますね。topleftの値は100pxというような文字列型で取得できるので、pxの部分を削除して100にして、さらに文字列型から数値型にNumber()で型変換してあげます。そうしないと計算が働きません。

#target {
	position: relative ;
	top: 0 ;
	left: 0 ;
}
// ドラッグ操作中のイベント
drag.on( "drag", function() {
	// イベントオブジェクト
	var event = d3.event ;

	// 前回発生時から今回発生時の間に動いた距離
	var dx = event.dx ;
	var dy = event.dy ;

	// 現在のleftの値(例:100px)から、[px]だけを削除して、[Number()]で数値型に変換する
	var left = Number( this.style.left.replace( "px", "" ) ) ;

	// 現在のtopの値(例:100px)から、[px]だけを削除して、[Number()]で数値型に変換する
	var top = Number( this.style.top.replace( "px", "" ) ) ;

	// [left]に動いた分(dx)だけを足す
	d3.select( this ).style( "left", left + dx + "px" ) ;

	// [top]に動いた分(dy)だけを足す
	d3.select( this ).style( "top", top + dy + "px" ) ;
} ) ;

このコードを確認する

ドラッグ可能範囲の計算

前章までで、要素をドラッグ操作で動かす仕組みが分かったと思います。ここで説明した方法以外にも、もっとスムーズな方法があるかもしれないので考えてみて下さいね。…さて、せっかくだからもう少しだけ深入りしてみましょう。先ほどの例だと、要素は画面を超えて、どこまでも動かせてしまいます。これでは不恰好ですよね。そこで、親要素の枠を超えないように処理を加えてみましょう。まずは、上下左右に指定できるtopleftの値の限界を考えてみましょうか。

限界値の算出方法
項目説明
親要素の上端から、要素の上側の距離までを、topにマイナス値として指定できる。例えば、親要素の上端と要素の上側までの距離が50pxの場合、topに指定できるのは-50pxまでである。
親要素の下端から、要素の下側の距離までを、topにプラス値として指定できる。例えば、親要素の下端と要素の下側までの距離が500pxの場合、topに指定できるのは500pxまでである。
親要素の左端から、要素の左側の距離までを、leftにマイナス値として指定できる。例えば、親要素の左端と要素の左側までの距離が150pxの場合、leftに指定できるのは-150pxまでである。
親要素の右端から、要素の右側の距離までを、leftにプラス値として指定できる。例えば、親要素の右端と要素の右側までの距離が250pxの場合、leftに指定できるのは250pxまでである。

それぞれの限界値をJavaScriptで算出するには、次の通りです。offsetTopoffsetLeftはそれぞれ親要素の上端、左端からの距離を参照できるプロパティなんですが、親要素にposition: relativeを指定しておかないと上手く働きません。スタイルシートで加えておきましょう。

#parent {
	position: relative ;
}
// ドラッグする要素をelementとする

// 上の限界値 (親要素の上端までの距離)
var topLimit = element.offsetTop ;

// 下の限界値 ( 親要素の高さ - 要素の高さ - 初期の上端までの距離 )
var bottomLimit = element.parentNode.clientHeight - element.clientHeight - topLimit ;

// 左の限界値 (親要素の左端までの距離)
var leftLimit = element.offsetLeft ;

// 右の限界値 ( 親要素の横幅 - 要素の横幅 - 初期の左端までの距離 )
var rightLimit = element.parentNode.clientWidth - element.clientWidth - leftLimit ;

topleftの値が、それぞれの限界値を超えていた場合に値を修正するように処理を加えればいいですね。

// 新しくtop、leftに設定する値をそれぞれnewTop、newLeftとする

// topのマイナス限界値を超えないように大きい方を採用
newTop = Math.max( newTop, -1 * topLimit ) ;

// topのプラス限界値を超えないように小さい方を採用
newTop = Math.min( newTop, bottomLimit ) ;

// leftのマイナス限界値を超えないように大きい方を採用
newLeft = Math.max( newLeft, -1 * leftLimit ) ;

// leftのプラス限界値を超えないように小さい方を採用
newLeft = Math.min( newLeft, rightLimit ) ;

// それぞれを1行にまとめると次の通り
// newTop = Math.min( Math.max( newTop, -1 * topLimit ), bottomLimit ) ;
// newLeft = Math.min( Math.max( newLeft, -1 * leftLimit ), rightLimit ) ;

以上をまとめると、最終的には次のコードになりました。親要素の枠を出ないように変わっているのを確認してみましょう。

// ドラッグする要素をelementとする
var element = d3.select( "#target" ).node() ;

// 上の限界値 (親要素の上端までの距離)
var topLimit = element.offsetTop ;

// 下の限界値 ( 親要素の高さ - 要素の高さ - 初期の上端までの距離 )
var bottomLimit = element.parentNode.clientHeight - element.clientHeight - topLimit ;

// 左の限界値 (親要素の左端までの距離)
var leftLimit = element.offsetLeft ;

// 右の限界値 ( 親要素の横幅 - 要素の横幅 - 初期の左端までの距離 )
var rightLimit = element.parentNode.clientWidth - element.clientWidth - leftLimit ;

// ドラッグ操作中のイベント
drag.on( "drag", function() {
	// イベントオブジェクト
	var event = d3.event ;

	// 前回発生時から今回発生時の間に動いた距離
	var dx = event.dx ;
	var dy = event.dy ;

	// 現在のleft、topの値(例:100px)から、[px]だけを削除して、[Number()]で数値型に変換する
	var left = Number( this.style.left.replace( "px", "" ) ) ;
	var top = Number( this.style.top.replace( "px", "" ) ) ;

	// left、topに設定する値
	var newLeft = left + dx ;
	var newTop = top + dy ;

	// 限界値を超えないように修正
	newTop = Math.min( Math.max( newTop, -1 * topLimit ), bottomLimit ) ;
	newLeft = Math.min( Math.max( newLeft, -1 * leftLimit ), rightLimit ) ;

	// left、topに動いた分(dx、dy)だけを足す
	d3.select( this ).style( "left", newLeft + "px" ) ;
	d3.select( this ).style( "top", newTop + "px" ) ;
} ) ;

このコードを確認する

クリックイベントとの共存

もう少し、説明します。ドラッグイベントを設定すると他のイベント、例えばクリックが上手く認識されません。というのも、クリックイベントは、mousedown(マウスを押す)、またはtouchstart(タッチを開始する)のイベントが発生した後に、mouseup(マウスを離す)、またはtouchend(タッチを終える)というイベントが発生した時、成立するからです。これらのイベントは、ドラッグイベントと動作が重複するので衝突してしまいます。要は、ドラッグ操作を開始して、ドラッグ操作を終了したタイミングでクリックイベントが意図せずに発火してしまうということです。下記の通り、要素にクリックイベントを追加してみました。挙動を確認してみて下さいね。なお、スマホデバイスのタッチ操作にはd3内部で対応しているようなので、タッチデバイスではこの不具合は起こりません。マウスデバイスでご確認下さい。

// 要素にクリックイベントを設定
d3.select( "#target" ).on( "click", function() {
	// クリックしたら1秒だけ要素を青色にする
	d3.select( "#target" ).style( "background", "blue" ).transition().duration(0).delay( 1000 ).style( "background", "#D36015" ) ;
} ) ;

このコードを確認する

これではコンテンツがまともに成立しません。そこで、Static Click(何もしない時の静的なクリック)だけを条件としたクリックイベントをどうやって設定する、というのがこの章の本題です。それには、イベントオブジェクト(d3.event)のdefaultPreventedというプロパティを参照して下さい。このプロパティは、d3.event.preventDefault()が既に実行されたか否かを確認するためのプロパティです。ドラッグ操作を開始すると、ドラッグ操作と一緒に画面や要素が動かないように、内部でd3.event.preventDefault()が実行されます。これが実行される前か後かを知ることで、そのクリックが静的か否かを判定できるというわけです。先ほどのイベントに、if文で条件を加えます。defaultPreventedは、クリックイベント以外のハンドラにも応用できるので、活用しましょう。

// 要素にクリックイベントを設定
d3.select( "#target" ).on( "click", function() {
	// d3.event.preventDefault()がまだ実行されていなければ、実行
	if( d3.event.defaultPrevented == false ) {
		// クリックしたら1秒だけ要素を青色にする
		d3.select( "#target" ).style( "background", "blue" ).transition().duration(0).delay( 1000 ).style( "background", "#D36015" ) ;
	}
} ) ;

このコードを確認する