拖拉作業

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

Draggable屬性

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

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

  • 在想要拖拉的元素上設draggable為true。.
  • 在dragstart事件上註冊一個事件處理器。
  • 在dragstart事件發生時,加入拖拉需要攜帶的資料。

以下是一段簡單的範例。

<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屬性,只不過,如果沒有取消預設行為,更動並不會有甚麼影響。

丟放回饋

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

滑鼠指標提示雖然夠用了,不過有時我們還是會想做其他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,