PointerEventsを使って要素をドラッグで動かす
はじめに
JavaScriptで要素をドラッグして移動する簡単な方法 と 「JavaScriptで要素をドラッグして移動する簡単な方法」という記事が初耳だらけだった件 という記事を読んで、知らないことが多かったので書いてある内容を実際に試してみました。
内容としては PointerEvents
を使った実装で、今まで自分が書いていたコードに比べてとても簡単になっていたので自分の理解のためにまとめてみました。
PointerEvents とは
ポインターイベントは、ポインティング機器に対して発生する DOM イベントです。 これらは、マウス、ペンやスタイラス、(1 本以上の指でなどの)タッチなどのポインティング入力機器を処理する単一の DOM イベントモデルを作成するように設計されています。
とのことで、入力デバイスに関係なくイベントを扱えるようにするためのDOMイベントのようです。baselineでも It’s been available across browsers since July 2020.
となっているので、4年以上前から使えたようです。知りませんでした。
完成形
どんなものを作るのかイメージしやすいように、最初に完成形のコードと動きをおいておきます。青い円をドラッグでグリグリ動かすことができるようになっています。
今までのコードと比べてどのくらい簡単になっているかを紹介したいので、完成形のコードの解説の前に今まで自分がどんな感じで実装していたかを書いていきます。
今までの実装 (MouseEventsやTouchEventsを使った実装)
自分が一番最初に書いていたのはこんな感じです。
実装としては大体こんなことをしています。
- ドラッグ中かどうかのフラグ変数を用意して
mousedown, touchstart
とmouseup, touchend
のタイミングで切り替え - フラグ変数が
true
であればmousemove, touchmove
イベントで動かす-
dragStart
で取得した動く前の位置とmousemove, touchmove
イベントで取得できる位置で移動幅を計算する - 上記で計算したものを
transform
やtop, left
などのstyleに当てはめる
-
ただ、この実装には重大な欠陥があって、マウスを早く動かしたりしてカーソルが要素をはみ出してしまうとイベントが外れて移動されなくなります。普通に使っていて起こるのでこれでは使い物になりません。
改良したものが以下で、自分はこういった実装をするときはこのようなコードで実装していました。
変更点は、要素に付与していた mousemove, mouseup, touchmove, touchend
イベントを dragStart
のタイミングで window
や document
に付与することでカーソルが要素を外れてもイベントが継続されるようにします。
そして mouseup, touchend
のタイミングで removeEventListener
を行うまでがセットという実装をしていたので結構めんどくさかったです。
PointerEventsを使った実装
ここで最初に紹介した完成形のコードに戻って解説していきます。
完成形のコードのHTMLやCSSをのぞいたJavaScriptのコードは以下ですべてです。だいぶスッキリしました。
const circle = document.getElementById("circle");
let x = 0;
let y = 0;
const drag = (e) => {
if (e.buttons <= 0) return;
x += e.movementX;
y += e.movementY;
e.target.style.transform = `translate(${x}px, ${y}px)`;
e.target.setPointerCapture(e.pointerId);
};
circle.addEventListener("pointermove", drag);
自分的に嬉しいところは主に4つです。これは PointerEvents
だからできることと、もともと自分が知らなかっただけで今回知ったことも含まれています。
要素から外れてもイベントが継続する
e.target.setPointerCapture(e.pointerId);
setPointerCapture()
メソッドを使うと、ポインターデバイスの接触が要素から外れた場合でも要素がイベントを受信できるようになるようです。
そのため、window
にイベントを設定する必要がなくなります。
イベントキャプチャを明示的に解除する releasePointerCapture()
メソッドもありますが、pointerup
イベントのタイミングでイベントのキャプチャは解除されるらしいのでこの実装では設定していません。
ドラッグかどうかの判定が簡単
if (e.buttons <= 0) return;
PointerEventのプロパティに buttons
というものがあり、どのボタンを押しているかの状態を持っています。
0であれば何も押されていない状態なので、e.buttons <= 0
で何も押していない場合は早期リターンするようにしています。
なので、このコード以降の処理はドラッグ時の処理となります。
これで isDragging
のような変数が不要になります。
どのボタンを押しているかも詳しくわかるようになっていて、MDNなどを見ると確認できます。
これはMouseEventのプロパティを継承していて、MouseEventではもともと使うことができたようです。
TouchEventにはないのでタッチデバイスが対象外の環境だったら使えたんですが知りませんでした。
ポインティングデバイスとタッチデバイスの判別が不要
PointerEvent
であればポインティングデバイスとタッチデバイスそれぞれに対してイベントを設定しなくて良いのが嬉しいです。
movementXとmovementYが便利
x += e.movementX;
y += e.movementY;
pointermove
イベントでX方向Y方向にどれだけ移動したかを movementX
と movementY
のプロパティで取得できます。そのため、移動前の位置を使用して移動距離を計算せずに随時反映するだけでOKです。
これは buttons
と同様でMouseEventのプロパティを継承しているようです。
ちなみにサンプルコードはtransformで移動させていますが、top
や left
のプロパティで移動させる場合は e.target.offsetLeft
と e.target.offsetTop
を使ってこのように書くこともできます。
const circle = document.getElementById("circle");
const drag = (e) => {
if (e.buttons <= 0) return;
const x = e.target.offsetLeft + e.movementX
const y = e.target.offsetTop + e.movementY
e.target.style.left = `${x}px`
e.target.style.top = `${y}px`
e.target.setPointerCapture(e.pointerId);
};
circle.addEventListener("pointermove", drag);
まとめ
Pointer Events
便利なので使っていこうと思います。
Discussion