本文會一一說明拖拉各步驟的作業。
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/plain或image/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", "https://www.mozilla.org"); dt.setData("text/plain", "https://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("https://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事件內來取得的clientX和clientY的滑鼠座標資訊。
最後,應該要在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事件還是會觸發,不過事件中的screenX與screenY屬性會是丟放發生點的資訊。
當dragend事件結束傳遞後,拖拉作業也完成了。
[1] 在Gecko,如果被拖拉元素在拖拉作業還在進行中移動或刪除,那麼dragend事件就不會觸發。bug 460801