跳到主要內容
← 回到學習

學習 · 前端基礎

拖曳事件 (DragEvent)

目錄 · 5 個段落
  1. 01三個必懂事件
  2. 02完整範例(原生 JS)
  3. 03React 版本
  4. 04常見地雷
  5. 05實務建議

拖曳事件(DragEvent / HTML5 Drag and Drop)

TL;DR
三個核心事件:dragstart(來源)、dragover(目標,必須 preventDefault())、drop(目標完成接收)。沒呼叫 preventDefault() 的話 drop 不會觸發,這是最常踩的雷。

三個必懂事件

事件觸發時機觸發在哪必做
dragstart使用者開始拖拉一個 draggable="true" 的元素來源元素e.dataTransfer.setData() 存資料
dragover拖拉中經過某元素的「上方持續觸發」目標元素必須 e.preventDefault(),否則 drop 不觸發
drop使用者放開滑鼠目標元素e.preventDefault() + e.dataTransfer.getData() 取資料

完整範例(原生 JS)

<ul id="list">
  <li draggable="true" data-index="0">A</li>
  <li draggable="true" data-index="1">B</li>
  <li draggable="true" data-index="2">C</li>
</ul>
html
const list = document.getElementById('list');
 
list.addEventListener('dragstart', (e) => {
  // 1. 標記哪個 item 被拖
  e.dataTransfer.setData('text/plain', e.target.dataset.index);
  e.dataTransfer.effectAllowed = 'move';
});
 
list.addEventListener('dragover', (e) => {
  e.preventDefault(); // ⚠️ 必須!否則 drop 不會觸發
  e.dataTransfer.dropEffect = 'move';
});
 
list.addEventListener('drop', (e) => {
  e.preventDefault();
  const fromIndex = parseInt(e.dataTransfer.getData('text/plain'));
  const toIndex = parseInt(e.target.dataset.index);
  // ...交換邏輯
});
javascript

React 版本

function SortableList({ items, onReorder }: Props) {
  const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
 
  return (
    <ul>
      {items.map((item, i) => (
        <li
          key={item.id}
          draggable
          onDragStart={(e) => {
            setDraggedIndex(i);
            e.dataTransfer.effectAllowed = 'move';
          }}
          onDragOver={(e) => e.preventDefault()}
          onDrop={(e) => {
            e.preventDefault();
            if (draggedIndex !== null && draggedIndex !== i) {
              onReorder(draggedIndex, i);
            }
            setDraggedIndex(null);
          }}
        >
          {item.label}
        </li>
      ))}
    </ul>
  );
}
tsx
原始來源
Stack Overflow — Move items up in a list with click with reactjs

常見地雷

🪤 dragover 沒 preventDefault → drop 永遠不觸發

這是 80% 拖曳功能爆掉的原因。瀏覽器預設不允許 drop,要 listener 主動「我接受」才會觸發 drop 事件。

🪤 dataTransfer.setData('text/plain', ...) 必須是字串

不能直接傳 object,要先 JSON.stringify:

e.dataTransfer.setData('application/json', JSON.stringify(item));
// drop 時:
const item = JSON.parse(e.dataTransfer.getData('application/json'));
javascript

🪤 行動裝置(Touch)不會觸發 DragEvent

HTML5 Drag and Drop 只支援滑鼠,手機觸控完全不觸發。要支援手機:

  • 用 dnd-kit(React)/ Sortable.js(原生)等套件,內部會處理 Touch + Pointer events
  • 自己用 pointerdown / pointermove / pointerup 自訂

實務建議

用套件還是原生?
  • 簡單的列表排序:原生夠用,4-5 個 listener 就完成
  • 複雜場景(虛擬滾動、跨容器、多選、行動裝置):直接用套件,別自己寫

目錄