MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

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

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.

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

以下是一段簡單的範例。

<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鍵搭配滑鼠進行選擇。

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

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

開始拖曳

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

<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方法,傳入資料型態和資料,這樣就可以攜帶想要的資料了:

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

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

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

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會清除資料。

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

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

設定拖曳圖片

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

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

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

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

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讓圖片落在滑鼠指標中央。 

使用XUL panel元素作為拖曳圖片

Requires Gecko 9.0(Firefox 9.0 / Thunderbird 9.0 / SeaMonkey 2.6)

在Gecko上開發,比如說外掛或Mozllia應用程式,Gecko9.0(Firefox 9.0 / Thunderbird 9.0 / SeaMonkey 2.6)支援使用panel元素作為拖曳圖片,簡單將XUL panel元素傳入setDragImage方法即可。

試想下面這個 panel元素:

<panel id="panel" style="opacity: 0.6">
  <description id="pb">Drag Me</description>
</panel>

<vbox align="start" style="border: 1px solid black;" ondragstart="startDrag(event)">
  <description>Drag Me</description>
</vbox>

當使用者拖曳vbox 元素時,startDrag函數會被呼叫。

function startDrag(event) {
  event.dataTransfer.setData("text/plain", "<strong>Body</strong>");
  event.dataTransfer.setDragImage(document.getElementById("panel"), 20, 20);
}

我們用HTML格式的"<strong>Body</strong>"作為資料,然後用pnael元素作為圖片。

拖曳效果

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

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

event.dataTransfer.effectAllowed = "copy";

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

     只能移動或連結。

none
不允許任何作業。
copy
只能複製。
move
只能移動。
link
只有連結。
copyMove
只能複製或移動。
copyLink
只能複製或連結。
linkMove
all
複製、移動或連結皆可。

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所列舉的作業。

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

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

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

指定拖曳目標

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

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

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

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

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

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:

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便可以應用在拖曳目標元素上。

.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所存入的對應資料型態的資料,倘若沒有對應型態資料,那空字串就會被回傳。

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

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

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

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

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}屬性中,但它可以方便的取得第一個連結,如下:

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

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

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

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

 
 

參見

文件標籤與貢獻者

 此頁面的貢獻者: jackblackevo, foxbrush
 最近更新: jackblackevo,