超解説!要素のドラッグ移動を実装する|Pure JS

要素をドラッグして移動できるようにする方法を学んだので簡単にまとめる。

まずはここから

ドラッグ以前の問題として、どこの数値をいじったら要素の位置を変えられるのか?という疑問があるかも知れない。答えはTopとLeftだ。それぞれの値が変化したとき、要素はどのように移動するだろう。スライダーを用意したので確認してみよう。

ボタンも押してみよう

Topの値が増えると要素は下へ移動する。Leftの値が増えたときは右へ移動する。

上のボタンを押すと、要素の位置がリセットされて動かせなくなることに気がついただろうか。TopとLeftは、Positionの値がstaticだと無効になるのだ。staticは初期値なので、TopとLeftを使うならPositionも合わせて設定しなくてはいけない。

Top/Leftの値を変更すれば要素を動かすことができる。Top/Leftを使うときはPositionの値も変更する必要がある。こういうこと。

ドラッグとはどういう操作?

ところで、要素をドラッグして動かすとは具体的にどういう操作だっただろう。

ここからはマウス操作で話を進めてゆきます。タッチデバイスとマウス操作、両方に対応させるコードもあとで解説します。

①要素上でマウスボタンを押下する→②押下したままマウスカーソルを動かす→③マウスボタンを離したときドラッグが終了する。これがユーザー側の操作だ。

この操作をマウスイベントに置き換えると、①mousedownイベント→②mousemoveイベント→③mouseupイベント、となる。

ここで着目すべき点は、要素上でマウスボタンが押下されたあと、押下されたまま②と③につながることだ。要するに、mousemoveとmouseupイベントは、mousedownイベントに内包される形になるということ。

なぜmouseupが内包されるのか?あまりピンとこないかも知れない。これは実際コードを動かしてみるとよくわかるのだが、押下されただけでマウスカーソルが移動しなかった場合の処理がいるので、mouseupはmousedownの中に入っている必要がある。

コードにすると次のようになる。

element.addEventListener('mousedown', (e)=>{

	element.addEventListener('mousemove', (e)=>{});
	element.addEventListener('mouseup', (e)=>{});

});

コールバックの引数eには、イベントが発生した時点でのマウスカーソルの座標や、それに関連するデータが入っている。引数から得られる座標を要素のTop/Leftの値に代入すれば、マウスカーソルに追従させることができそうだ。

このとき必要になるのは、マウスカーソルが動いたときの座標である。なので、mousemoveイベントのコールバック内で処理する必要がある。実際のコードは次の項で。

トライアンドエラー

不完全バージョン

まあギリギリそれっぽくなっているだろうか。でもだいぶ動作がおかしい。問題点は2つある。

1つは、マウスカーソルが速く動いたときに要素から外れてしまい、そのままになることだ。こうなる原因は、動かす要素にmousemoveのイベントリスナを登録しているから。mousemoveイベントのコールバックは非常に短い間隔で実行されるのだが、それでもマウスカーソルの動きにぴったり付いていくことはできないのだ。

この問題に対処するには、要素が移動できる領域全体を含む要素にmousemoveのイベントリスナを登録すればいい。手っ取り早いのはdocument、あるいはbody。

2つ目の問題は、マウスカーソルが要素の左上の角に重なっていることだ。マウスカーソルの座標をそのままTop/Leftに代入しているから当然っちゃあ当然なのだが、これは格好悪いので直したい。

マウスカーソルの座標をTop/Leftに代入している
要素の角がカーソルの座標に引き寄せられている

理想的な動作としては、要素とマウスカーソルの相対位置を維持したまま、要素を動かすことだろう。これをするには、最初にマウスボタンが押下された時点で、マウスカーソルの座標と要素の左上の座標とのギャップを計算し、Top/Leftに座標を代入するときにギャップを差し引けばよい。

ややこしすぎるので、とりあえず図を見て欲しい。

マウスカーソルの座標から要素の端までの距離
ギャップとはマウスカーソルの座標から要素の端までの距離
マウスカーソルの座標からビューポートの端までの距離
clientX/Yの値

ギャップを求めるには、↑から↓を差し引けばよい。

要素の端からビューポートの端までの距離
getBoundingClientRect().top/leftの値

ギャップを求めるためには、引数eに入っているclientX/Yの座標と、Element.getBoundingClientRect().top/left座標が必要になる。

clientX/Yは、マウスカーソルからビューポートの端までの距離を表す。

Element.getBoundingClientRect().top/leftは、要素の端からビューポートの端までの距離を表す。

これら2種類の値はどちらもビューポートに相対する値なので、差分を取ることができる。ちなみにビューポートというのはブラウザの表示領域のこと。

ギャップの値がわかったら、あとはTop/Leftに座標を代入するときにギャップを差し引くだけだ。問題点を修正したコードは次のとおり。

完全なコードと補足

完成バージョン

とりあえず完成でいいんじゃないだろうか。少しだけ補足する。

el.style.left = (e.pageX - gapX) + 'px';
el.style.top = (e.pageY - gapY) + 'px';

上のコードはclientX/Yでも動作するのだが、名前が重なるのでpageX/Yを使ってみた。pageX/Yは、マウスカーソルの座標からdocumentの端までの距離を表す。つまりはマウスカーソルの座標からページ全体の端までの距離。

ここではJSFiddleのiframe内でコードを実行しているので、documentの端もビューポートの端も同じ位置になる。よって、pageX/YとclientX/Yの値も同じになるためどちらでも動作するのだ。でも実際に組み込むときは違う値になることが多いはずなので、どちらの値を使用するべきか考える必要があるかと思う。

それから、最初に出たposition:absoluteの値は、祖先要素にposition:relativeが設定されていると、祖先要素を基準とした値になる。要素にも相対関係があるので気をつけたい。

タッチデバイスにも対応させる

ということで、最後にタッチデバイスにも対応させる方法について。タッチデバイスは次のコードで検出できる。

const touchEventsEnabled = Boolean(window.ontouchstart);

このフラグを使って対応するイベントを切り替えればいい。

element.addEventListener(touchEventsEnabled ? 'touchstart' : 'mousedown', (e)=>{

	document.addEventListener(touchEventsEnabled ? 'touchmove' : 'mousemove', (e)=>{});
	element.addEventListener(touchEventsEnabled ? 'touchend' : 'mouseup', (e)=>{});

});

おまけ

親要素の外にはみ出さないようにしたバージョン。

外側に飛び出さないバージョン

お知らせ

現在お知らせはありません。

最近の投稿

シェア