👆

PointerEventsを使って要素をドラッグで動かす

2025/01/26に公開

はじめに

JavaScriptで要素をドラッグして移動する簡単な方法「JavaScriptで要素をドラッグして移動する簡単な方法」という記事が初耳だらけだった件 という記事を読んで、知らないことが多かったので書いてある内容を実際に試してみました。

内容としては PointerEvents を使った実装で、今まで自分が書いていたコードに比べてとても簡単になっていたので自分の理解のためにまとめてみました。

PointerEvents とは

https://developer.mozilla.org/ja/docs/Web/API/Pointer_events

ポインターイベントは、ポインティング機器に対して発生する DOM イベントです。 これらは、マウス、ペンやスタイラス、(1 本以上の指でなどの)タッチなどのポインティング入力機器を処理する単一の DOM イベントモデルを作成するように設計されています。

とのことで、入力デバイスに関係なくイベントを扱えるようにするためのDOMイベントのようです。baselineでも It’s been available across browsers since July 2020. となっているので、4年以上前から使えたようです。知りませんでした。

完成形

どんなものを作るのかイメージしやすいように、最初に完成形のコードと動きをおいておきます。青い円をドラッグでグリグリ動かすことができるようになっています。

今までのコードと比べてどのくらい簡単になっているかを紹介したいので、完成形のコードの解説の前に今まで自分がどんな感じで実装していたかを書いていきます。

今までの実装 (MouseEventsやTouchEventsを使った実装)

自分が一番最初に書いていたのはこんな感じです。

実装としては大体こんなことをしています。

  • ドラッグ中かどうかのフラグ変数を用意して mousedown, touchstartmouseup, touchend のタイミングで切り替え
  • フラグ変数が true であれば mousemove, touchmove イベントで動かす
    • dragStart で取得した動く前の位置と mousemove, touchmove イベントで取得できる位置で移動幅を計算する
    • 上記で計算したものを transformtop, left などのstyleに当てはめる

ただ、この実装には重大な欠陥があって、マウスを早く動かしたりしてカーソルが要素をはみ出してしまうとイベントが外れて移動されなくなります。普通に使っていて起こるのでこれでは使い物になりません。

改良したものが以下で、自分はこういった実装をするときはこのようなコードで実装していました。

変更点は、要素に付与していた mousemove, mouseup, touchmove, touchend イベントを dragStart のタイミングで windowdocument に付与することでカーソルが要素を外れてもイベントが継続されるようにします。

そして 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 イベントのタイミングでイベントのキャプチャは解除されるらしいのでこの実装では設定していません。

https://developer.mozilla.org/ja/docs/Web/API/Element/setPointerCapture

ドラッグかどうかの判定が簡単

if (e.buttons <= 0) return;

PointerEventのプロパティに buttons というものがあり、どのボタンを押しているかの状態を持っています。

0であれば何も押されていない状態なので、e.buttons <= 0 で何も押していない場合は早期リターンするようにしています。

なので、このコード以降の処理はドラッグ時の処理となります。
これで isDragging のような変数が不要になります。

どのボタンを押しているかも詳しくわかるようになっていて、MDNなどを見ると確認できます。

https://developer.mozilla.org/ja/docs/Web/API/Pointer_events#ボタンの状態の判断

これはMouseEventのプロパティを継承していて、MouseEventではもともと使うことができたようです。
TouchEventにはないのでタッチデバイスが対象外の環境だったら使えたんですが知りませんでした。

https://developer.mozilla.org/ja/docs/Web/API/MouseEvent/buttons

ポインティングデバイスとタッチデバイスの判別が不要

PointerEvent であればポインティングデバイスとタッチデバイスそれぞれに対してイベントを設定しなくて良いのが嬉しいです。

movementXとmovementYが便利

x += e.movementX;
y += e.movementY;

pointermove イベントでX方向Y方向にどれだけ移動したかを movementXmovementY のプロパティで取得できます。そのため、移動前の位置を使用して移動距離を計算せずに随時反映するだけでOKです。

これは buttons と同様でMouseEventのプロパティを継承しているようです。

https://developer.mozilla.org/ja/docs/Web/API/MouseEvent/movementX

ちなみにサンプルコードはtransformで移動させていますが、topleft のプロパティで移動させる場合は e.target.offsetLefte.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 便利なので使っていこうと思います。

RightTouch

Discussion