TypeScriptのドラッグ&ドロップ:HTML5 Drag and Drop API入門

スポンサーリンク

TypeScriptのドラッグ&ドロップ:HTML5 Drag and Drop API入門

ライブラリなしにドラッグ&ドロップを実装する最小コードです。

基本実装:要素を別の場所に移動する

<!-- index.html -->
<div id="source" draggable="true">ドラッグしてね</div>
<div id="target">ここにドロップ</div>
const source = document.getElementById('source')!
const target = document.getElementById('target')!

// ドラッグ開始:dataTransferにIDを渡す
source.addEventListener('dragstart', (event: DragEvent) => {
  event.dataTransfer?.setData('text/plain', source.id)
})

// dragover で preventDefault しないと drop が発火しない
target.addEventListener('dragover', (event: DragEvent) => {
  event.preventDefault()
})

// ドロップ:dataTransferからIDを取り出して要素を移動
target.addEventListener('drop', (event: DragEvent) => {
  event.preventDefault()
  const id = event.dataTransfer?.getData('text/plain')
  if (!id) return

  const dragged = document.getElementById(id)
  if (dragged) {
    target.appendChild(dragged)
  }
})

イベントの種類と発火順

イベント 発火するタイミング 対象
dragstart ドラッグ開始時 ドラッグ要素
drag ドラッグ中(連続) ドラッグ要素
dragenter ドロップ先に入った瞬間 ドロップ先
dragover ドロップ先の上にいる間 ドロップ先
dragleave ドロップ先から出た瞬間 ドロップ先
drop ドロップした瞬間 ドロップ先
dragend ドラッグ終了時 ドラッグ要素

TypeScriptでの型

ドラッグイベントの型は DragEvent です。

element.addEventListener('dragstart', (event: DragEvent) => {
  // event.dataTransfer は DataTransfer | null
  event.dataTransfer?.setData('text/plain', 'value')
})

event.dataTransfernull になりうるため、オプショナルチェーン(?.)を使います。


ドラッグ中のスタイル変更

ドラッグ中の要素と、ドロップ可能なエリアにスタイルを当てると UX が上がります。

source.addEventListener('dragstart', (event: DragEvent) => {
  event.dataTransfer?.setData('text/plain', source.id)
  source.classList.add('dragging') // 透明化など
})

source.addEventListener('dragend', () => {
  source.classList.remove('dragging')
})

target.addEventListener('dragenter', (event: DragEvent) => {
  event.preventDefault()
  target.classList.add('drag-over') // 枠線を強調など
})

target.addEventListener('dragleave', () => {
  target.classList.remove('drag-over')
})

target.addEventListener('drop', (event: DragEvent) => {
  event.preventDefault()
  target.classList.remove('drag-over')
  // 移動処理
})

実用例:リストの並び替え

<ul id="list">
  <li draggable="true" data-id="1">アイテム 1</li>
  <li draggable="true" data-id="2">アイテム 2</li>
  <li draggable="true" data-id="3">アイテム 3</li>
</ul>
const list = document.getElementById('list')!
let draggedItem: HTMLElement | null = null

list.addEventListener('dragstart', (event: DragEvent) => {
  draggedItem = event.target as HTMLElement
  draggedItem.classList.add('dragging')
})

list.addEventListener('dragend', () => {
  draggedItem?.classList.remove('dragging')
  draggedItem = null
})

list.addEventListener('dragover', (event: DragEvent) => {
  event.preventDefault()
  const target = (event.target as HTMLElement).closest('li')
  if (!target || target === draggedItem) return

  // ドラッグ中のアイテムをターゲットの前後に挿入
  const rect = target.getBoundingClientRect()
  const isAfter = event.clientY > rect.top + rect.height / 2

  if (isAfter) {
    target.after(draggedItem!)
  } else {
    target.before(draggedItem!)
  }
})

マウスの縦位置(event.clientY)とターゲット要素の中央を比較して、前後どちらに挿入するかを判定しています。


リスト間でアイテムを移動する

2つのリスト間でアイテムを移動する例です。

<ul id="list-a">
  <li draggable="true">A-1</li>
  <li draggable="true">A-2</li>
</ul>
<ul id="list-b">
  <li draggable="true">B-1</li>
</ul>
let dragging: HTMLElement | null = null

document.querySelectorAll('li').forEach((li) => {
  li.addEventListener('dragstart', () => {
    dragging = li
  })
})

document.querySelectorAll('ul').forEach((ul) => {
  ul.addEventListener('dragover', (event: DragEvent) => {
    event.preventDefault()
  })

  ul.addEventListener('drop', (event: DragEvent) => {
    event.preventDefault()
    if (dragging) {
      ul.appendChild(dragging) // ドロップ先のリストに移動
    }
  })
})

よくあるハマりポイント

drop が発火しない

dragoverevent.preventDefault() を呼んでいないと drop は発火しません。必須です。

// これがないと drop が動かない
target.addEventListener('dragover', (event) => {
  event.preventDefault() // 忘れずに
})

draggable 属性が必要

ドラッグしたい要素に draggable="true" を設定しないとドラッグできません。<img><a> はデフォルトでドラッグ可能です。

event.target の型キャスト

event.targetEventTarget | null なので、HTMLElement にキャストして使います。

const el = event.target as HTMLElement

dragleave が子要素で発火する

ドロップ先に子要素がある場合、子要素に移動しただけでも dragleave が発火します。dragenterdragleave のカウンターで管理するか、pointer-events: none を子要素に当てる方法があります。

let enterCount = 0

target.addEventListener('dragenter', () => {
  enterCount++
  target.classList.add('drag-over')
})

target.addEventListener('dragleave', () => {
  enterCount--
  if (enterCount === 0) {
    target.classList.remove('drag-over')
  }
})

target.addEventListener('drop', () => {
  enterCount = 0
  target.classList.remove('drag-over')
})

まとめ

やること コード
ドラッグ可能にする <div draggable="true">
ドラッグ開始時にデータを渡す event.dataTransfer?.setData('text/plain', value)
ドロップを許可する dragoverevent.preventDefault()
ドロップ時にデータを受け取る event.dataTransfer?.getData('text/plain')
ドラッグ中の要素を取得 event.target as HTMLElement
  • dragoverpreventDefault() を忘れると drop が発火しない
  • ドラッグ中のスタイル変更は dragstart/dragend で classList を操作する
  • 並び替えには clientYgetBoundingClientRect() の組み合わせが定番