此頁面由社群從英文翻譯而來。了解更多並加入 MDN Web Docs 社群。

View in English Always switch to English

拖曳操作

本文會一一說明拖曳各步驟的作業。

The drag operations described in this document use the DataTransfer interface. This document does not use the DataTransferItem interface nor the DataTransferItemList interface.

Draggable 屬性

網頁中有些預設的拖曳行為,例如文字選擇、圖片或超連結,當拖曳圖片或超連結時,圖片或超連結的 URL 會被當作拖曳作業中所攜帶的資料,而其他類型元素則必須另外處理才能拖曳,試試看選擇網頁某一部分,然後按住滑鼠鍵來進行拖曳,依據 OS 不同,或許會有一些跟著滑鼠移動的效果,但這僅僅只是預設效果行為,實際上沒有任何資料跟著被拖曳。

In HTML, apart from the default behavior for images, links, and selections, no other elements are draggable by default. In XUL, all elements are draggable.

除了文字選擇、圖片或超連結之外,沒有元素預設是可拖曳的。所以要讓一個元素可以拖曳,有幾件事必須要做:

以下是一段簡單的範例。

html
<div
  draggable="true"
  ondragstart="event.dataTransfer.setData('text/plain', 'This text may be dragged')">
  This text <strong>may</strong> be dragged.
</div>

draggable 為 true 後,該 DIV 元素便可以拖曳,反之,倘若 draggable 為 false 或無設定則不可拖曳,只有其中下含的文字可以被選擇。draggable 屬性適用於任何元素,一般來說預設為 false,除了圖片和連結預設為 true,所以說如果想要阻止圖片和連結被拖曳,則可以設定 draggable 為 false。

請注意,一旦元素被定為可拖曳之後,其下內含的文字或其他元素便無法像平常一樣用滑鼠選擇,使用者之能夠改用鍵盤或按住 Alt 鍵搭配滑鼠進行選擇。

至於 XUL 元素則是預設皆可拖曳。

html
<button
  label="Drag Me"
  ondragstart="event.dataTransfer.setData('text/plain', 'Drag Me Button');"></button>

開始拖曳

下方範例在 dragstart 註冊一個事件處理器。

html
<div
  draggable="true"
  ondragstart="event.dataTransfer.setData('text/plain', 'This text may be dragged')">
  This text <strong>may</strong> be dragged.
</div>

當拖曳作業開始,dragstart 事件會觸發,然後我們可以在事件處理器中準備好我們所要攜帶的資料、想要的拖曳回饋效果,不過基本上其實只需要準備資料就好,因為預設拖曳回饋效果已經足以應付大多數的狀況,此外,我們也可以改在上一層父元素註冊事件處理器,因為拖曳事件會上向傳遞 ( Bubble up ) 。

拖曳資料

所有的拖曳事件物件都有一個 dataTransfer 屬性,這個屬性是用來攜帶資料。

當拖曳時,資料必須和被拖曳目標作連結,比如說拖曳文字框中反白選擇的文字,那麼文字本身便是連結資料,同理,拖曳連結時 URL 便是連結資料。

資料包含兩個部分,一是資料型態(或格式)、二是資料值。所謂資料型態是用文字描述資料型態(如text/plain代表文字資料),而資料值則是文字,要加入拖曳資料需要提供資料的型態和內容值;有了資料後,我們可以在 dragenter 或 dragover 事件處理器中,透過檢查資料型態來決定是否可以接受後續的放置操作,比如說只接受連結類資料的拖曳目標區(drop target),會檢查資料型態是否為text/uri-list

資料型態符合 MIME 型態,如text/plainimage/jpeg等等,而我們自己也可以自定義其他型態,最常使用的型態請見推薦拖曳資料型態

一趟拖曳作業中可以攜帶多個多種型態的資料,所以我們可以自定義自己的型態同時,還提供其他資料給不認得自定義資料型態的其他拖曳目標區使用。通常最通用的資料會是文字類型資料。

呼叫setData方法,傳入資料型態和資料,這樣就可以攜帶想要的資料了:

js
event.dataTransfer.setData("text/plain", "Text to drag");

上例資料是「Text to drag」文字,型態是 text/plain。

呼叫多次 setData 我們就可以攜帶多種資料。

js
var dt = event.dataTransfer;
dt.setData("application/x-bookmark", bookmarkString);
dt.setData("text/uri-list", "http://www.mozilla.org");
dt.setData("text/plain", "http://www.mozilla.org");

這裡加入了三種資料,第一種是自定義的「application/x-bookmark」,雖然有更豐富的內容可使用,但只有我們自己認識,而另外我們又為其他網站或應用加入了兩種比較常見的資料,「text/uri-list」以及「text/plain」。

如果對同一種資料型態加入兩次資料,則新加資料會取代舊資料。

呼叫clearData會清除資料。

js
event.dataTransfer.clearData("text/uri-list");

如果呼叫 clearData 時有傳入資料型態,則只會清除該型態資料,如果沒有傳入任何型態,則所有資料皆會被清除。

設定拖曳圖片

當拖曳進行中,以拖曳元素為基礎,一個半透明的圖片會自動產生出來,並且跟著滑鼠移動。如果想要,我們也可以呼叫 setDragImage() 來指定我們自己的拖曳使用圖片。

js
event.dataTransfer.setDragImage(image, xOffset, yOffset);

setDragImage 需要三個參數,一是圖片來源(通常是圖片元素,但也可以是 canvas 元素或其他元素),拖曳使用圖片會依照圖片來源在螢幕上所顯示的樣子產生;二和三是圖片相對於滑鼠指標的位置位移量。

不過也是能夠使用文件外部的圖片或 canvas 元素,當需要透過 canvas 元素產生客製圖片時,這個技巧很有用,如下範例所示:

js
function dragWithCustomImage(event) {
  var canvas = document.createElementNS(
    "http://www.w3.org/1999/xhtml",
    "canvas",
  );
  canvas.width = canvas.height = 50;

  var ctx = canvas.getContext("2d");
  ctx.lineWidth = 4;
  ctx.moveTo(0, 0);
  ctx.lineTo(50, 50);
  ctx.moveTo(0, 50);
  ctx.lineTo(50, 0);
  ctx.stroke();

  var dt = event.dataTransfer;
  dt.setData("text/plain", "Data to Drag");
  dt.setDragImage(canvas, 25, 25);
}

上面我們的 canvas 是 50 x 50px 大小,然後我們位移一半 25 讓圖片落在滑鼠指標中央。

拖曳效果

拖曳作業有好機種;copy 作業代表被拖曳資料會被複製一份到拖曳目標區,move 作業代表移動被拖曳的資料,link 作業代表拖曳來源區和拖曳目標區有某種關係。

在 dragstart 事件中可以設定effectAllowed屬性,指定拖曳源頭允許的作業。

js
event.dataTransfer.effectAllowed = "copy";

上面只有 copy 被允許,但還有其他種類:

只能移動或連結。

none

不允許任何作業。

copy

只能複製。

move

只能移動。

只有連結。

copyMove

只能複製或移動。

只能複製或連結。

linkMoveall

複製、移動或連結皆可。

effectAllowed 屬性預設所有作業都接受,如 all 值。

在 dragenter 或 dragover 事件中,我們可以藉由檢查 effectAllowed 來知道那些作業是被允許的,另外,另一個相關聯的dropEffect屬性應該要是 effectAllowed 的其中一個作業,但是 dropEffect 不接受多重作業,只可以是 none, copy, move 和 link。

dropEffect 屬性會在在 dragenter 以及 dragover 事件中初始化為使用者想要執行的作業效果,使用者能夠透過按鍵(依平台不同,通常是 Shift 或 Ctrl 鍵),在複製、移動、連接作業之間切換,同時滑鼠指標也會跟著相應變換,例如複製作業的滑鼠旁會多出一個+的符號。

effectAllowed 和 dropEffect 屬性可以在 dragenter 或 dragover 事件中更改,更改 effectAllowed 屬性能讓拖曳作業只能在支援被允許作業類型的拖曳目標上執行,好比說 effectAllowed 為 copyMove 的作業就會阻止使用者進行 link 類型的作業。

我們也可以更改 dropEffect 來強迫使用者執行某項作業,而且應該要是 effectAllowed 所列舉的作業。

js
event.dataTransfer.effectAllowed = "copyMove";
event.dataTransfer.dropEffect = "copy";

上面的範例中 copy 就是會被執行的作業效果。

若 effectAllowed 或 dropEffect 為 none,那麼沒有放置作業可以被執行。

指定拖曳目標

dragenter 和 dragover 事件就是用來指定拖曳目標區,也就是放置資料的目標區,絕大多數的元素預設的事件都不准放置資料。

所以想要放置資料到元素上,就必須取消預設事件行為。取消預設事件行為能夠藉由回傳 false 或呼叫event.preventDefault方法。

html
<div ondragover="return false">
  <div ondragover="event.preventDefault()"></div>
</div>

通常我們只有在適當的時機點才需要呼叫 event.preventDefault 方法、取消預設事件行為,比如說被拖曳進來的是連結。所以檢查被拖曳進來的物件,當符合條件後再來取消預設事件行為。

藉由檢查拖曳資料型態來決定是否允許放置,是最常見的作法。dataTransfer 物件的types屬性是一個拖曳資料型態的列表,其中順序按照資料被加入之先後排序。

js
function doDragOver(event) {
  var isLink = event.dataTransfer.types.contains("text/uri-list");
  if (isLink) event.preventDefault();
}

上面我們呼叫 contains 方法檢察 text/uri-list 是否存在拖曳資料型態的列表之內,有的話才取消預設行為、准許放置作業,否則,不取消預設行為、不准許放置作業。

檢查拖曳資料型態後,我們也可以依此更動 effectAllowed 和 dropEffect 屬性,只不過,如果沒有取消預設行為,更動並不會有甚麼影響。

Updates to DataTransfer.types

Note that the latest spec now dictates that DataTransfer.types should return a frozen array of DOMStrings rather than a DOMStringList (this is supported in Firefox 52 and above).

As a result, the contains method no longer works on the property; the includes method should be used instead to check if a specific type of data is provided, using code like the following:

js
if ([...event.dataTransfer.types].includes("text/html")) {
  // Do something
}

You could always use some feature detection to determine which method is supported on types, and run code as appropriate.

放置回饋

有好幾種方法回饋使用者,告訴使用者甚麼元素可以接受放置作業,最簡單的是滑鼠會指標會自動變換樣式(視平台而定)。

滑鼠指標提示雖然夠用了,不過有時我們還是會想做其他 UI 上的樣式變化。-moz-drag-over 的 CSS pseudo-class 便可以應用在拖曳目標元素上。

css
.droparea:-moz-drag-over {
  border: 1px solid black;
}

當目標元素的 dragenter 預設事件有被取消時,這個 pseudo-class 就會啟動,目標 UI 會套用 1px 的黑色 border,請注意 dragover 並不會檢查這項設定。

其他比如說插入圖片等,在 dragenter 事件內執行更多更複雜的樣式變化也是可以的。

倘若想要做出圖片更著滑鼠在拖曳目標區上面移動的效果,那麼可以在 dragover 事件內來取得的clientXclientY的滑鼠座標資訊。

最後,應該要在 dragleave 事件內復原之前所做樣式變更,dragleave 事件不需要取消預設事件行為、永遠都會觸發,即使拖曳被取消了;至於使用-moz-drag-over 的 CSS 方法的話,樣式復原會自動執行,不用擔心。

執行放置作業

當使用者在拖曳目標區上放開滑鼠時,drop 事件就會觸發。當 drop 事件發生,我們需要取出被丟入的資料,然後處理之。

要取出被丟入的資料,那就要呼叫 dataTransfer 物件的getData方法。getData 方法接受資料型態的參數,它會回傳setData所存入的對應資料型態的資料,倘若沒有對應型態資料,那空字串就會被回傳。

js
function onDrop(event) {
  var data = event.dataTransfer.getData("text/plain");
  event.target.textContent = data;
  event.preventDefault();
}

上面的範例會取出文字資料,假設拖曳目標區是文字區域,例如 p 或 div 元素,那麼資料就會被當作文字內容,插入目標元素之中。

網頁之中,如果我們已經處理過放置資料,那應該要呼叫{preventDefault}方法防止瀏覽器再次處理資料,比如說,Firefox 預設是會開啟拖入的連結,但我們可以取消這項預設行為來避免開啟連結。

當然也可以取得其他種類資料來使用,比如說連結資料,text/uri-list

js
function doDrop(event)
{
  var links = event.dataTransfer.getData("text/uri-list").split("\n");
  for each (var link in links) {
    if (link.indexOf("#") == 0)
      continue;

    var newlink = document.createElement("a");
    newlink.href = link;
    newlink.textContent = link;
    event.target.appendChild(newlink);
  }
  event.preventDefault();
}

上面的範例取得連結資料,然後生成連結元素、加入頁面。從 text/uri-list 字面上不難猜出這種資料是一行行的 URL,所以我們呼叫 split 方法拆開一行行的 URL,再將 URL 一個一個加入頁面。請注意我們有避開開頭為「#」字元的註解。

更簡單的作法是採用特別 URL 型態。URL 型態是一個特殊簡寫用途形態,它不會出現在{types}屬性中,但它可以方便的取得第一個連結,如下:

js
var link = event.dataTransfer.getData("URL");

這個作法能夠省去檢查註解和一個一個掃過 URL,但只會得到第一個 URL。

下面的例子會從多個支援的資料型態中,找出支援的資料。

js
function doDrop(event)
{
  var types = event.dataTransfer.types;
  var supportedTypes = ["application/x-moz-file", "text/uri-list", "text/plain"];
  types = supportedTypes.filter(function (value) types.contains(value));
  if (types.length)
    var data = event.dataTransfer.getData(types[0]);
  event.preventDefault();
}

完成拖曳

拖曳作業完成後,不論成功或取消於否,被拖曳元素的dragend事件都會觸發,如果想要判別作業是否完成,可以檢查 dropEffect 屬性,若是 dropEffect 為 none,代表拖曳作業被取消,否則 dropEffect 的值代表所完成的作業類型。

有一個 Gecko 專屬的mozUserCancelled屬性,當使用者按 ESC 鍵取消拖曳後,這個屬性會為 true,但若是因其他理由被取消或成功,則為 false

拖曳作業的放置可以發生在同一個視窗或其他應用程式,而且 dragend 事件還是會觸發,不過事件中的screenXscreenY屬性會是放置發生點的資訊。

當 dragend 事件結束傳遞後,拖曳作業也完成了。

[1] 在 Gecko,如果被拖曳元素在拖曳作業還在進行中移動或刪除,那麼 dragend 事件就不會觸發。bug 460801

參見