JavaScriptで要素をドラッグして移動する簡単な方法は使ってはいけない

JavaScriptで要素をドラッグ

先週、JavaScriptで要素をドラッグして移動する簡単な方法という記事がありました。

<img id="$img" src="https://js.cx/clipart/ball.svg" width="40" height="40">
<script>
$img.onpointermove = function(event){
    if(event.buttons){
        this.style.left     = this.offsetLeft + event.movementX + 'px'
        this.style.top      = this.offsetTop  + event.movementY + 'px'
        this.style.position = 'absolute'
        this.draggable      = false
        this.setPointerCapture(event.pointerId)
    }
}
</script>

mousedown+mousemove+mouseup+フラグ変数を駆使して実現するコードはよくありますが複雑になるので、ドラッグするだけなら上記コードを使いましょう。

JavaScriptで要素をドラッグして移動するサンプル(オリジナル)

しかし、上記のコードにはいくつかの問題がありますので、そのまま使うことはオススメしません。

問題1 idに$imgが使用されている

idに$imgが使用されており直接「$img.onpointermove」でイベントを処理しています。

これはブラウザのグローバルスコープの動作によります。ブラウザでは、IDを持つHTML要素が自動的にグローバルスコープに変数として追加されるためです。

しかし、このようなやり方は一般的ではなく、グローバル変数の名前衝突のリスクがあるため、明示的にdocument.getElementByIdを使用したほうが良いです。

// <img id="ball" src="https://js.cx/clipart/ball.svg" width="40" height="40">
const ball = document.getElementById('ball')

ball.onpointermove = function(event) {
  // 中略
}

問題2 デフォルトでposition: absolute;が指定されていない

onpointermoveイベントが発生してから「position: absolute;」が適用されていますが、positionプロパティのデフォルト値はstaticなので、これだとドラッグ時に表示崩れが発生します。

そのため、あらかじめドラッグする要素には「position: absolute;」を適用しておいたほうが良いです。

#ball {
  position: absolute;
}

問題3 ドラッグ時のカーソルが変わらない

ドラッグする要素にマウスオーバーしたときとドラッグしたときのカーソルがデフォルトのままなので、ドラッグ中であることが視覚的にわかりづらいです。

要素に「cursor: grab;」を適用して、ドラッグ中は「cursor: grabbing;」が適用されるようにすれば、ドラッグ中であることがわかりやすくなります。

#ball {
  position: absolute;
  cursor: grab;
}
this.style.cursor = 'grabbing'

JavaScriptで要素をドラッグして移動するサンプル(修正版)

問題4 z-indexの値が指定されていない

ドラッグする要素にz-indexの値が指定されていないため、ドラッグ時にほかの要素の下に表示されてしまいます。

もし、ドラッグする要素を常に最前面に表示させたい場合は「z-index: calc(Infinity);」を指定してください。

#ball {
  position: absolute;
  cursor: grab;
  z-index: calc(Infinity);
}

問題5 要素がブラウザの表示範囲外に配置されてしまう

ドラッグした際に要素の位置と移動距離を取得して配置していますが、このままでは制限なしでどこにでも配置できてしまいます。

ブラウザの表示範囲外にも配置できるため、表示範囲外に配置された場合、再びドラッグすることができなくなってしまいます。

以下のコードのようにブラウザの表示範囲を取得し、表示範囲外に配置されないように処理すれば、この問題を解決できます。

const ball = document.getElementById('ball')

ball.onpointermove = function(event) {
  if (event.buttons) {
    const screenWidth = window.innerWidth
    const screenHeight = window.innerHeight

    this.style.left = Math.max(0, Math.min(this.offsetLeft + event.movementX, screenWidth - this.offsetWidth)) + 'px'
    this.style.top = Math.max(0, Math.min(this.offsetTop + event.movementY, screenHeight - this.offsetHeight)) + 'px'
    this.style.cursor = 'grabbing'
    this.draggable = false
    this.setPointerCapture(event.pointerId)
  } else {
    this.style.cursor = 'grab'
  }
}

JavaScriptで要素をドラッグして移動するサンプル(完成版)