新視野行銷企劃

拖放 (Drag & Drop) 功能的完整教學

「拖放 (Drag & Drop) 功能的完整教學」封面圖,扁平化設計風格,展示手部拖曳圖片至網頁區塊的畫面,象徵 HTML5 與 JavaScript 拖放功能的操作教學,適用於網頁開發教學文章。
拖放 (Drag & Drop) 是現代網頁中極為直覺且實用的互動功能。想像一下,使用者可以直接拖曳圖片以重新排序相簿,或將待辦事項從「進行中」列表移動到「已完成」列表,這種直接操作的體驗大大提升了介面友好性。對中級程度的開發者而言,掌握拖放技術可以為專案增添豐富的互動效果。本文將以原生 JavaScript 從零開始,帶領您深入理解拖放的基本原理、事件綁定方法,並提供桌面與行動裝置皆適用的實作範例。同時,我們也會分享一些最佳實踐與常見錯誤的避免方法,最後以問與答單元解答開發中常見的疑惑。現在,讓我們一起探索拖放功能的奧秘吧!

基本原理說明

拖放功能的背後,其實是一系列滑鼠事件或觸控事件在作祟。整個流程牽涉「可拖曳元素」、「拖曳資料」以及「放置目標」三個要素:首先是使用者想要拖動的元素,其次是在拖動過程中可以攜帶的資料(例如被拖元素的ID或內容),最後是允許放下的目標區域。當使用者開始拖曳元素時,瀏覽器會產生一個拖曳影像隨滑鼠移動,而在這期間會觸發多個關鍵事件,包括 dragstart(開始拖曳)、dragover(拖曳經過放置目標上方)、drop(釋放拖曳,在目標放下)以及 dragend(結束拖曳)等。這些事件各司其職,共同完成一次拖放操作。

一般來說,我們可以透過設定元素的 HTML5 draggable 屬性來讓它變得可拖曳(預設下,圖片、連結等元素自帶可拖曳性,其他元素則需手動設定)。而在使用 JavaScript 處理拖放時,常見的步驟是:

  • 設定可拖曳元素: 將目標元素的 draggable 設為 true,並監聽 dragstart 事件,在拖曳開始時準備好要傳輸的資料。例如,可以利用事件物件的 dataTransfer 接口,透過 setData 方法保存被拖元素的資訊(如元素ID或其他識別資料),供稍後在 drop 時取用。
  • 允許放置: 預設情況下,大部分元素不允許成為放置目標。為了讓拖曳元素可以放下到某個區域,我們需要在潛在的目標元素上監聽 dragover 事件,並呼叫 event.preventDefault() 來取消瀏覽器預設行為,表示當前區域允許拖入。
  • 處理放下事件: 在放置目標上監聽 drop 事件,當使用者釋放滑鼠時觸發。在這個事件的處理器中,我們使用 event.dataTransfer.getData 取回先前保存的資料,據此取得被拖曳的元素,然後透過DOM操作將該元素附加到目標區域中,完成拖放。

以下是一個簡單的例子,展示上述基本原理:一個方塊可以被拖曳,並放入一個目標容器中。

HTML + JavaScript
<!-- HTML結構 -->
<div id="draggable-item" draggable="true" style="width:100px; height:100px; background:skyblue;">
  拖曳我!
</div>
<div id="drop-zone" style="width:200px; height:150px; border:2px dashed #ccc; margin-top:10px;">
  放下到這裡
</div>

<script>
// 取得元素引用
const dragItem = document.getElementById('draggable-item');
const dropZone = document.getElementById('drop-zone');

// 拖曳開始時,設定拖曳資料
dragItem.addEventListener('dragstart', event => {
  event.dataTransfer.setData('text/plain', event.target.id);
});

// 拖曳經過放置目標時,允許放下
dropZone.addEventListener('dragover', event => {
  event.preventDefault(); // 必須取消預設行為才能觸發 drop
});

// 放下拖曳元素時,將元素移動到目標容器
dropZone.addEventListener('drop', event => {
  event.preventDefault();
  const data = event.dataTransfer.getData('text/plain');
  const draggedElement = document.getElementById(data);
  if (draggedElement) {
    dropZone.appendChild(draggedElement);
  }
});
</script>

在這段程式中,我們首先將一個方塊設為可拖曳,接著在 dragstart 時透過 dataTransfer.setData 存入被拖元素的資訊(這裡存的是元素的ID)。dropZone 部分則在 dragover 時取消預設行為以允許放置,最終在 drop 事件中取出資料,找到對應的元素節點並將它附加到放置容器中。透過這幾步,方塊就能順利被拖放到虛線框內。了解了拖放的基本概念和事件流程後,我們就可以進一步探討如何在程式碼中正確綁定這些事件,以打造更複雜的拖放功能。

事件綁定方法

要實現順暢的拖放效果,正確地綁定和處理事件是關鍵。在上一節範例中,我們透過 addEventListener 為元素綁定了 dragstart、dragover 和 drop 等事件。這種將行為與元素分離的做法使程式碼組織更清晰,也方便我們在需要時對多個元素批次註冊事件。

1. 綁定拖曳事件到多個元素

在實務中,拖放功能通常涉及一組元素,而不只單一對象。例如,有多個項目都可被拖曳、多個區域都可作為放置目標。此時可以利用迴圈批次將事件處理函式附加到一組元素上。以下範例展示如何為多個 draggable 項目和多個 drop 區域綁定事件:

JavaScript
const items = document.querySelectorAll('.draggable-item');  // 所有可拖曳項目
const zones = document.querySelectorAll('.drop-zone');       // 所有放置區域

items.forEach(item => {
  item.addEventListener('dragstart', onDragStart);
  item.addEventListener('dragend', onDragEnd);
});

zones.forEach(zone => {
  zone.addEventListener('dragover', onDragOver);
  zone.addEventListener('drop', onDrop);
  zone.addEventListener('dragenter', onDragEnter);
  zone.addEventListener('dragleave', onDragLeave);
});

上面的程式碼使用了 forEach 對選取到的元素群組逐一附加事件監聽。其中,我們監聽了幾個常用的拖放相關事件:

  • dragstart: 用於在拖曳開始時執行初始化邏輯。例如設定拖曳資料、改變外觀提示(如降低透明度或改變邊框顏色提示已被選取)。
  • dragend: 在拖曳結束(不論是否成功放下)時觸發,可在此恢復元素外觀或進行清理工作。
  • dragover: 在拖曳物件經過放置目標上方時連續觸發。我們通常只在此使用 event.preventDefault() 來允許 drop,但也能藉此實時反饋目前拖曳位置,例如動態顯示佔位標記。
  • drop: 當拖曳物件在目標上放下時觸發,負責完成拖放的核心動作(如插入或交換元素)。
  • dragenter 和 dragleave: 當拖曳物件進入或離開目標區域時觸發。我們可利用這兩個事件來做一些視覺效果,例如高亮顯示目前的可放置區域、或在物件離開時移除高亮。

接著,我們來看看對應的事件處理函式該如何撰寫:

JavaScript - 事件處理函式
function onDragStart(event) {
  // 設定拖曳資料 (使用元素的id作為識別)
  event.dataTransfer.setData('text/plain', event.target.id);
  // 視覺效果:拖曳時降低透明度
  event.target.style.opacity = '0.5';
}

function onDragEnd(event) {
  // 還原被拖曳元素的透明度
  event.target.style.opacity = '1';
}

function onDragOver(event) {
  event.preventDefault(); // 必須取消預設行為,否則無法觸發 drop
}

function onDrop(event) {
  event.preventDefault();
  // 取得被拖曳元素的id並找到對應元素
  const id = event.dataTransfer.getData('text/plain');
  const draggedEl = document.getElementById(id);
  if (draggedEl) {
    // 將被拖元素移到這個放置區中 (追加到最後)
    event.currentTarget.appendChild(draggedEl);
  }
}

function onDragEnter(event) {
  // 目標區域呈現可放置提示
  event.currentTarget.classList.add('drop-highlight');
}

function onDragLeave(event) {
  // 移除目標區域的提示樣式
  event.currentTarget.classList.remove('drop-highlight');
}

以上程式碼示範了一套常見的拖放事件處理流程:在 onDragStart 中我們透過 dataTransfer 保存資料並改變元素樣式提示「正在被拖曳」;onDragEnd 將樣式復原;onDragOver 單純用於允許放置;onDrop 則根據保存的資料找到被拖曳的元素並將其移動到新的容器(透過 event.currentTarget 獲知觸發 drop 的目標容器);onDragEnter/onDragLeave 對目標容器加上或移除 CSS 類,提供使用者即時的視覺反饋。例如,我們可以在 CSS 中定義 .drop-highlight 來改變邊框或背景,讓使用者清楚看見自己可以把元素「放」在哪裡。

透過上述方式,我們能將多個元素的拖放行為統一處理,清楚地將資料傳遞、DOM 操作和視覺反饋分開管理。現在,我們已經建立了拖放的事件基礎架構,接下來就可以把這些拼圖組合起來,實現一個更貼近實際情境的互動範例。

範例應用

為了鞏固對拖放機制的理解,我們來實作一個實際的互動範例:拖放重新排序待辦事項。想像有兩個列表,一個是「待處理事項 (To-Do)」,另一個是「已完成事項 (Done)」。使用者可以拖曳每一項任務在兩個列表間移動,或者在同一列表內重新排序。這個範例涵蓋了拖放的兩種典型應用:在不同容器間移動元素,以及在同一容器內改變順序。

首先,我們建立HTML結構,每個任務項目 (li) 都標記為可拖曳,兩個列表 (ul) 則作為放置區域:

HTML
<ul id="todo-list" class="list">
  <li id="task1" class="task-item" draggable="true">???? 待辦事項 1</li>
  <li id="task2" class="task-item" draggable="true">???? 待辦事項 2</li>
  <li id="task3" class="task-item" draggable="true">???? 待辦事項 3</li>
</ul>

<ul id="done-list" class="list">
  <li id="done1" class="task-item" draggable="true">✅ 完成事項 A</li>
  <li id="done2" class="task-item" draggable="true">✅ 完成事項 B</li>
</ul>

接著是CSS的一點點設定,以利於觀察(這部分您也可以依需求調整樣式):

CSS
.list {
  width: 45%;
  min-height: 150px;
  margin: 0 1%;
  padding: 10px;
  display: inline-block;
  vertical-align: top;
  border: 2px dashed #aaa;
}

.task-item {
  background: #f0f0f0;
  margin: 5px 0;
  padding: 8px;
  border: 1px solid #ccc;
  cursor: grab;             /* 提示可拖曳 */
}

.task-item:active {
  cursor: grabbing;         /* 拖曳時的鼠標樣式 */
}

.drop-highlight {
  background: #e0ffe0;      /* 放置區域高亮(淡綠底) */
}

現在讓我們撰寫 JavaScript,實現拖放排序與移動的邏輯:

JavaScript - 完整拖放排序實現
const tasks = document.querySelectorAll('.task-item');
const lists = document.querySelectorAll('.list');
let draggedItem = null;  // 用於儲存目前拖曳的項目

tasks.forEach(item => {
  item.addEventListener('dragstart', event => {
    draggedItem = item;  // 紀錄拖曳中的項目
    event.dataTransfer.setData('text/plain', item.id);
    event.dataTransfer.effectAllowed = 'move';
    // 縮小被拖曳項目的不透明度,表示正在拖動
    item.style.opacity = '0.5';
  });
  item.addEventListener('dragend', () => {
    // 拖曳結束時還原透明度
    item.style.opacity = '1';
    draggedItem = null;
  });
  // 每個任務項目本身也作為drop target以支援在項目間放置
  item.addEventListener('dragover', event => {
    event.preventDefault(); // 允許在另一項目上方放置
  });
  item.addEventListener('drop', event => {
    event.preventDefault();
    if (draggedItem) {
      const currentList = event.currentTarget.parentNode;
      // 將拖曳的項目插入到當前項目之前
      currentList.insertBefore(draggedItem, event.currentTarget);
    }
  });
});

lists.forEach(list => {
  list.addEventListener('dragover', event => {
    event.preventDefault();  // 允許放置到列表
  });
  list.addEventListener('drop', event => {
    event.preventDefault();
    if (draggedItem) {
      // 如果在列表空白處放下,將項目移動到該列表底部
      list.appendChild(draggedItem);
    }
  });
  list.addEventListener('dragenter', event => {
    // 高亮顯示可放置的列表區域
    event.currentTarget.classList.add('drop-highlight');
  });
  list.addEventListener('dragleave', event => {
    // 移除高亮樣式
    event.currentTarget.classList.remove('drop-highlight');
  });
});

在上述程式中,我們實現了以下功能:

  • 拖曳任務項目: 當使用者開始拖曳任務 (dragstart) 時,我們將該項目記錄在 draggedItem 變數中,以便稍後在 drop 時引用。此外,透過 event.dataTransfer.setData 保存被拖曳元素的ID,並設定 effectAllowed = 'move' 以提示這是移動操作。同時,我們降低了該項目的透明度,給予視覺上的「抓起來了」效果。
  • 在項目上方放置 (重新排序): 每個 .task-item 本身也監聽了 dragover 和 drop。這使得使用者拖動一個項目到另一個項目上方時,可以觸發那個項目的 drop。在 item.addEventListener('drop') 中,我們取得目前拖曳的項目 draggedItem,然後透過 DOM 操作將它插入到目標項目 (event.currentTarget) 的前面。如此即可在同一列表中改變順序。例如,拖動「待辦事項 3」到「待辦事項 1」上方,鬆開後「3」就會被插入到「1」之前。
  • 在列表空白處放置 (跨列表移動): 列表容器 .list 監聽了 dragover 和 drop。當使用者將項目拖到某列表區的空白處(非直接在某個項目上)放開時,就會觸發該列表的 drop。我們在這裡判斷 draggedItem 存在後,直接呼叫 list.appendChild(draggedItem) 將項目移動到對應列表的最後位置。這實現了跨容器移動任務的功能。例如,把「待辦事項 2」直接拖到右側的「已完成事項」列表空白處,鬆手後「事項 2」就會從左列表移除並出現在右列表底部。
  • 目標區域高亮: 透過 dragenter 和 dragleave,我們在列表容器上添加或移除 .drop-highlight 類,使當前可放置的列表以淡綠背景顯示,方便使用者辨識拖曳目標。

這個範例將拖放的核心概念應用於實際情境,展示了如何同時處理重新排序和跨區域移動。您可以試著在瀏覽器中執行這段程式碼,體驗拖放排序待辦事項的效果。當然,在真實專案中,我們可能還需要針對細節進行調整,例如限制哪些項目可拖放到哪些區域、或是在拖放時提供更多視覺提示等等。不過到目前為止,我們的範例已經可以在桌面環境中順利運作了。那麼,如果使用者在行動裝置上操作呢?下一節我們將探討如何讓拖放功能支援觸控螢幕。

行動裝置支援方式

隨著行動裝置的普及,確保拖放功能在手機和平板上可用變得十分重要。然而,開發者常會發現:在某些行動瀏覽器上,前述的 HTML5 拖放事件並不如預期觸發。事實上,傳統的 dragstart/dragover/drop API 主要基於滑鼠事件,在行動裝置上(特別是手機瀏覽器)並沒有原生支援拖放事件。因此,我們需要採取額外的措施,讓拖放行為在觸控螢幕上「被感知」。

有幾種策略可以讓拖放支援行動裝置:

1. 使用觸控事件模擬拖放

既然手機上無法依賴瀏覽器幫我們發出拖放事件,我們可以改用原生的觸控事件(Touch Events)或較新的指標事件(Pointer Events)自行實現拖放邏輯。基本思路是監聽 touchstart(手指觸碰開始)取得目標元素,接著監聽 touchmove 隨著手指移動來拖動元素,最後在 touchend(手指放開)時判斷放下的位置並完成元素的移動。下面範例展示如何使用觸控事件來實現一個簡化的拖放:

JavaScript - 觸控事件拖放
let activeElement = null;
let offsetX = 0, offsetY = 0;

// 為可拖曳元素監聽觸控開始
draggableElement.addEventListener('touchstart', event => {
  const touch = event.touches[0];
  activeElement = event.currentTarget;
  // 記錄初始座標與元素位置差,讓元素跟隨手指偏移一致
  offsetX = touch.clientX - activeElement.getBoundingClientRect().left;
  offsetY = touch.clientY - activeElement.getBoundingClientRect().top;
  // 暫時以絕對定位,才能移動位置
  activeElement.style.position = 'absolute';
  // 為避免觸發畫面滾動,取消預設行為
  event.preventDefault();
});

draggableElement.addEventListener('touchmove', event => {
  if (!activeElement) return;
  const touch = event.touches[0];
  // 更新被拖曳元素的位置,使其跟隨手指移動
  activeElement.style.left = (touch.clientX - offsetX) + 'px';
  activeElement.style.top = (touch.clientY - offsetY) + 'px';
  // 可選:透過 elementFromPoint 找出當前手指下的元素,進行放置區域高亮等操作
  const target = document.elementFromPoint(touch.clientX, touch.clientY);
  // ... 可以在這裡對 target 判斷,加入高亮類或其他提示
  event.preventDefault();
});

draggableElement.addEventListener('touchend', event => {
  if (!activeElement) return;
  // 手指放開時,確認落點目標
  const touch = event.changedTouches[0];
  const dropTarget = document.elementFromPoint(touch.clientX, touch.clientY);
  if (dropTarget && dropTarget.classList.contains('list')) {
    // 將拖曳元素附加到目標列表(容器)中
    dropTarget.appendChild(activeElement);
  }
  // 放置完畢後,清除狀態並還原元素位置模式
  activeElement.style.position = 'static';
  activeElement = null;
});

上述程式透過 touchstart 抓取被拖曳元素,利用 touchmove 不斷更新元素的 left/top,讓它追隨手指游標。當觸控結束 (touchend) 時,我們使用 elementFromPoint 找到手指最後停留位置下的元素,藉此判斷元素被放在何處(例如是否在某個 .list 容器上)。若是,我們將 activeElement append 進那個容器,模擬完成一次拖放操作。最後將元素的定位方式改回靜態佈局以融入正常文檔流。需要注意,為了避免拖動畫面導致頁面捲動,我們在 touchstart 和 touchmove 都調用了 event.preventDefault()。

2. 採用 Pointer Events

Pointer Events 是較新的網頁API,它將滑鼠、觸控、手寫筆等輸入方式統一在一組事件中 (pointerdown, pointermove, pointerup 等),瀏覽器會自動處理不同裝置的對應行為。使用 Pointer Events,我們可以寫一套事件處理邏輯,同時滿足桌面和行動裝置。然而,需要注意的是,即便使用 Pointer Events,我們依然是在模擬拖放行為(手動移動元素位置),而不是依賴瀏覽器提供的 drag/drop 機制。因此使用 Pointer Events 的程式碼實質上會和上面的觸控實作類似,只是事件名稱不同(例如用 pointerdown 代替 touchstart/mousedown)。

3. 借助現成的 Polyfill 或函式庫

如果自行實現觸控拖放邏輯過於繁瑣,也可以考慮使用社群提供的解決方案。舉例來說,有一些輕量級的 polyfill 可以將行動裝置的觸控操作轉換為標準的 HTML5 拖放事件,使您原本寫好的拖放程式碼無需修改就能在手機上運行。此外,像 Hammer.js 這類專門處理觸控手勢的函式庫,也能輔助我們監聽平移(pan)手勢,再配合調整 DOM 達成拖放效果。但由於本教學著重於原生 JavaScript 實作,我們並未使用任何外部函式庫;這類工具可視專案需求選擇性採用。

在支援行動裝置時,有幾點需要特別留意:一是觸控操作沒有像滑鼠懸停那樣的概念,因此在手機上沒有 dragenter/leave 的對應狀態,我們需要自行透過位置判斷實現類似效果(例如使用 elementFromPoint 持續偵測手指所在位置);二是確保在觸控拖曳時,不會因為頁面捲動或縮放等手勢衝突而導致體驗問題(這也是為什麼我們在觸控事件中常常會呼叫 preventDefault() 來阻止瀏覽器的預設行為)。總而言之,實作行動版拖放考驗著對事件的掌控與對DOM操作的即時性,但只要掌握了上述技巧,就能讓拖放功能順利地跨越裝置限制,提供一致的使用者體驗。

現在,我們已經具備了在桌面與行動裝置上實現拖放的完整知識。接下來,讓我們了解一些開發拖放功能的最佳實踐,以及常見的陷阱與錯誤,以確保您的實作更加完善穩健。

最佳實踐與常見錯誤

最佳實踐

  • 提供視覺反饋: 在拖放過程中給予使用者明確的視覺提示,例如拖曳元素本身變化(大小或透明度改變)、目標區域高亮、或使用自訂的拖曳影像。良好的視覺反饋能讓使用者清楚瞭解目前的操作狀態,大幅提升體驗。
  • 合理運用 CSS Cursor: 透過 CSS 設定 cursor: grab 及 cursor: grabbing,讓滑鼠游標在可拖曳元素上呈現對應手勢圖示,符合用戶直覺。同時,可利用 dragover 時修改 event.dataTransfer.dropEffect 來反映允許的操作類型,例如移動 (move) 或複製 (copy) 等,使使用者知道放下後會發生什麼。
  • 保持 DOM 結構簡潔: 拖放操作往往涉及頻繁的 DOM 更新(插入、移除節點)。建議在實作時保持 HTML 結構的簡單與語義清晰,例如使用列表 (<ul>/<li>) 來呈現可排序項目,有助於操作的可靠性和未來維護。在更新 DOM 時,也要謹慎處理,避免一次拖放中產生太多不必要的重排 (reflow) 或重繪 (repaint) 開銷。
  • 考量無障礙體驗: 原生拖放功能對鍵盤及輔助技術的支援較弱。若有需要,應提供替代操作方式,像是為鍵盤使用者提供上下移動項目的按鈕或其他機制,確保每個使用者都能完成相同的操作。
  • 適度限制拖放目標: 在某些情況下,並非所有區域都應該接受所有拖曳項目。良好的實踐是根據業務規則在 drop 事件中檢查目標是否合法(例如透過 event.target 或 dropTarget.id 判斷),不符合條件則拒絕放置並還原元素原始位置。這可以避免使用者將元素拖放到不恰當的地方導致資料錯亂。
  • 清理事件與狀態: 確保在拖放結束後(無論成功或中止),相關的狀態標記和樣式都被重置。例如前面範例中在 dragend/touchend 時將透明度復原、drop-highlight 樣式移除。若使用全域變數暫存拖曳對象,也應及時清空,避免殘留的狀態影響下一次操作。

常見錯誤

  • 遺漏 event.preventDefault() 導致無法放置: 一個很常見的疏忽是在放置區的 dragover 事件中沒有調用 preventDefault()。這會使瀏覽器認為不允許在該區域放下,導致 drop 事件壓根不會觸發。遇到 drop 無效時,首先應檢查是否正確地在 dragover 中取消了預設行為。
  • 忘記設置 draggable 屬性: 非內建可拖曳的元素(如一般的 <div> 或 <li>)若未加上 draggable="true",是無法啟動拖曳的。這個屬性經常被忽略,導致事件沒反應。務必確認所有預期可拖曳的元素都正確設置了此屬性。
  • 在行動裝置上無響應: 如前所述,直接依賴 HTML5 拖放 API 的實作在行動裝置上可能不起作用。如果您發現拖放功能在手機上失靈,請記得必須實作觸控事件或使用 Pointer Events 來加以支援。這並非程式錯誤,而是瀏覽器支援差異,需要額外處理。
  • 拖曳資料未正確傳遞: 使用 dataTransfer 時,常見錯誤包括:在 dragstart 中漏掉設定資料,或是在 drop 中取得資料時拼寫錯誤導致拿不到值。另外,setData 可以設定不同類型的資料 (例如 'text/plain'、'text/uri-list' 等),讀取時類型要對應;若類型不一致也會取不到資料。
  • 未考慮元素ID唯一性: 有些實作依賴元素的 id 來傳遞拖放資訊(例如我們範例中透過元素ID傳遞)。若頁面上存在重複的ID,可能會發生錯將元素移動到錯誤位置的情況。在設計拖放功能時,確保使用的識別方法能唯一對應元素,或者直接傳遞元素引用(拖放在同一頁面內時,也可直接將 DOM 節點透過變數閉包捕獲,而非用ID查找)。
  • 忽視放置失敗的情況: 有時使用者可能在拖曳過程中取消操作(如按下 ESC 鍵)或將元素拖出瀏覽器視窗之外再鬆手。對此類中止或失敗的情況,最好能在 dragend 事件中加入處理,例如將元素移回原位或移除暫存的拖曳樣式。如果完全不處理,可能遺留視覺上的不一致(例如元素半透明留在原處)或狀態沒清理乾淨的問題。

瞭解並避開以上這些常見的錯誤陷阱,將有助於你構建一個健全且穩定的拖放功能。最後,讓我們通過問與答的形式,快速回顧並解答幾個在開發拖放功能時經常遇到的問題。

問與答單元

問:拖放時,如何自訂拖動時跟隨游標的那個預覽圖像?
答: 瀏覽器在拖曳開始時會自動產生一個半透明的預覽圖像(通常是被拖元素的複製)。若想自訂這個圖像,可以在 dragstart 事件中使用 event.dataTransfer.setDragImage(element, x, y) 方法。你可以傳入一個隱藏的 DOM 元素或動態建立的 <canvas>/<img> 作為自訂圖像,後兩個參數則是游標相對於該圖像的熱點座標。例如,為了使用自訂圖片作為拖曳預覽,可以這樣做:

JavaScript
const img = new Image();
img.src = 'custom-drag.webp';
event.dataTransfer.setDragImage(img, 0, 0);

這樣拖曳時跟隨游標的就會是 custom-drag.webp 這張圖片了。

問:我可以一次拖曳多個元素嗎?
答: 原生的拖放 API 對一次拖曳多個元素的支援相對有限。瀏覽器的 dataTransfer 介面有一個 items 屬性,可在 dragstart 時透過 event.dataTransfer.items 添加多項資料。不過實務上,開發者更常見的做法是實作多選機制:例如透過按住 Ctrl/Shift 鍵點選選取多個項目,然後在開始拖曳時,程式碼中收集所有選取項目的資訊,一同放入 dataTransfer,或者乾脆在 drop 時由程式去批量處理先前標記為選取的元素。簡而言之,可以做到拖曳多項目,但需要自行管理選取和搬移的過程,瀏覽器不會自動處理多項目的拖放。

問:如何限制拖放只能在特定區域發生?
答: 如果我們想限制使用者只能將元素拖放到特定容器,例如防止拖到其他無關區域,可以採取兩種措施:(1) 視覺提示:在不允許放置的區域不高亮或顯示禁止圖示,讓使用者明白那裡不能放。(2) 邏輯判斷:在 drop 事件處理函式中檢查目標容器的身份,如果不符合條件,則忽略該次放置操作。例如:

JavaScript
dropZone.addEventListener('drop', event => {
  if (event.currentTarget.id !== 'allowed-zone') return; // 非允許區域直接返回
  // ...合法區域的處理...
});

搭配以上兩點,就能有效限制拖放僅發生在我們指定的區域中。

問:拖放排序後,我如何保存更新後的順序?
答: 如果拖放操作會改變資料的順序(比如我們的待辦事項排序範例),那麼在操作完成後通常需要將新的順序保存下來。做法取決於您的應用程式架構:

在前端,可以遍歷容器內的項目順序,讀出ID或內容,儲存在 localStorage 或透過 AJAX 呼叫傳回後端。這樣即使重新整理頁面,也能從儲存的順序載入項目。

在後端(若有的話),可以在每次 drop 完成時,用 fetch/axios 等將新的順序傳給伺服器數據庫儲存。無論前後端,關鍵是在 drop 或 dragend 時機觸發保存邏輯,例如更新一個清單陣列的順序並存檔。下次載入頁面時,再根據保存的順序重建列表即可。

問:有沒有什麼簡單的方法可以讓元素只能左右(或上下)拖動?
答: 原生的 HTML5 拖放 API 本身不提供軸向限制——因為實際上,HTML5 拖放並不是透過改變元素位置來移動的,而是一種資料轉移機制。如果你想實現僅水平或僅垂直拖動的效果,建議採用自行處理位置的方式(如使用滑鼠或觸控事件)。你可以監聽 mousedown 然後在 mousemove 中更新元素的位置,但僅改變 X 或 Y 座標。例如僅允許橫向拖動:element.style.left = event.clientX + 'px',但 Y 保持不變。這類實現等同於自製拖拽,而非使用瀏覽器的拖放機制。需要注意手動實現時要處理好mouseup時停止拖動,以及避免游標移出元素外的狀況(可參考 Pointer Events 來更方便地實作)。

總結

通過本教程的學習,我們從原理到實作,完整體驗了拖放功能的開發流程。對中級開發者而言,拖放不僅僅是幾個事件的組合,更蘊含著對使用者體驗的考量與跨裝置支援的挑戰。希望這篇教學能讓您在專案中自信地運用原生 JavaScript 打造出順暢又專業的拖放互動效果。現在,是時候運用所學,為您的使用者帶來更加直觀、有趣的操作體驗了!

CONTACT US

網站設計報價洽詢

請填寫您的資料,我們將儘快與您聯繫! 為必填