拖拉事件的種類
拖拉(drag)指的是,用戶在某個對象上按下鼠標鍵不放,拖動它到另一個位置,然后釋放鼠標鍵,將該對象放在那里。
拖拉的對象有好幾種,包括元素節(jié)點、圖片、鏈接、選中的文字等等。在網(wǎng)頁中,除了元素節(jié)點默認不可以拖拉,其他(圖片、鏈接、選中的文字)都可以直接拖拉。為了讓元素節(jié)點可拖拉,可以將該節(jié)點的draggable
屬性設為true
。
<div draggable="true">
此區(qū)域可拖拉
</div>
上面代碼的div
區(qū)塊,在網(wǎng)頁中可以直接用鼠標拖動。松開鼠標鍵時,拖動效果就會消失,該區(qū)塊依然在原來的位置。
draggable
屬性可用于任何元素節(jié)點,但是圖片(<img>
)和鏈接(<a>
)不加這個屬性,就可以拖拉。對于它們,用到這個屬性的時候,往往是將其設為false
,防止拖拉這兩種元素。
注意,一旦某個元素節(jié)點的draggable
屬性設為true
,就無法再用鼠標選中該節(jié)點內(nèi)部的文字或子節(jié)點了。
當元素節(jié)點或選中的文本被拖拉時,就會持續(xù)觸發(fā)拖拉事件,包括以下一些事件。
drag
:拖拉過程中,在被拖拉的節(jié)點上持續(xù)觸發(fā)(相隔幾百毫秒)。dragstart
:用戶開始拖拉時,在被拖拉的節(jié)點上觸發(fā),該事件的target
屬性是被拖拉的節(jié)點。通常應該在這個事件的監(jiān)聽函數(shù)中,指定拖拉的數(shù)據(jù)。dragend
:拖拉結(jié)束時(釋放鼠標鍵或按下 ESC 鍵)在被拖拉的節(jié)點上觸發(fā),該事件的target
屬性是被拖拉的節(jié)點。它與dragstart
事件,在同一個節(jié)點上觸發(fā)。不管拖拉是否跨窗口,或者中途被取消,dragend
事件總是會觸發(fā)的。dragenter
:拖拉進入當前節(jié)點時,在當前節(jié)點上觸發(fā)一次,該事件的target
屬性是當前節(jié)點。通常應該在這個事件的監(jiān)聽函數(shù)中,指定是否允許在當前節(jié)點放下(drop)拖拉的數(shù)據(jù)。如果當前節(jié)點沒有該事件的監(jiān)聽函數(shù),或者監(jiān)聽函數(shù)不執(zhí)行任何操作,就意味著不允許在當前節(jié)點放下數(shù)據(jù)。在視覺上顯示拖拉進入當前節(jié)點,也是在這個事件的監(jiān)聽函數(shù)中設置。dragover
:拖拉到當前節(jié)點上方時,在當前節(jié)點上持續(xù)觸發(fā)(相隔幾百毫秒),該事件的target
屬性是當前節(jié)點。該事件與dragenter
事件的區(qū)別是,dragenter
事件在進入該節(jié)點時觸發(fā),然后只要沒有離開這個節(jié)點,dragover
事件會持續(xù)觸發(fā)。dragleave
:拖拉操作離開當前節(jié)點范圍時,在當前節(jié)點上觸發(fā),該事件的target
屬性是當前節(jié)點。如果要在視覺上顯示拖拉離開操作當前節(jié)點,就在這個事件的監(jiān)聽函數(shù)中設置。drop
:被拖拉的節(jié)點或選中的文本,釋放到目標節(jié)點時,在目標節(jié)點上觸發(fā)。注意,如果當前節(jié)點不允許drop
,即使在該節(jié)點上方松開鼠標鍵,也不會觸發(fā)該事件。如果用戶按下 ESC 鍵,取消這個操作,也不會觸發(fā)該事件。該事件的監(jiān)聽函數(shù)負責取出拖拉數(shù)據(jù),并進行相關處理。
下面的例子展示,如何動態(tài)改變被拖動節(jié)點的背景色。
div.addEventListener('dragstart', function (e) {
this.style.backgroundColor = 'red';
}, false);
div.addEventListener('dragend', function (e) {
this.style.backgroundColor = 'green';
}, false);
上面代碼中,div
節(jié)點被拖動時,背景色會變?yōu)榧t色,拖動結(jié)束,又變回綠色。
下面是一個例子,展示如何實現(xiàn)將一個節(jié)點從當前父節(jié)點,拖拉到另一個父節(jié)點中。
/* HTML 代碼如下
<div class="dropzone">
<div id="draggable" draggable="true">
該節(jié)點可拖拉
</div>
</div>
<div class="dropzone"></div>
<div class="dropzone"></div>
<div class="dropzone"></div>
*/
// 被拖拉節(jié)點
var dragged;
document.addEventListener('dragstart', function (event) {
// 保存被拖拉節(jié)點
dragged = event.target;
// 被拖拉節(jié)點的背景色變透明
event.target.style.opacity = 0.5;
}, false);
document.addEventListener('dragend', function (event) {
// 被拖拉節(jié)點的背景色恢復正常
event.target.style.opacity = '';
}, false);
document.addEventListener('dragover', function (event) {
// 防止拖拉效果被重置,允許被拖拉的節(jié)點放入目標節(jié)點
event.preventDefault();
}, false);
document.addEventListener('dragenter', function (event) {
// 目標節(jié)點的背景色變紫色
// 由于該事件會冒泡,所以要過濾節(jié)點
if (event.target.className === 'dropzone') {
event.target.style.background = 'purple';
}
}, false);
document.addEventListener('dragleave', function( event ) {
// 目標節(jié)點的背景色恢復原樣
if (event.target.className === 'dropzone') {
event.target.style.background = '';
}
}, false);
document.addEventListener('drop', function( event ) {
// 防止事件默認行為(比如某些元素節(jié)點上可以打開鏈接),
event.preventDefault();
if (event.target.className === 'dropzone') {
// 恢復目標節(jié)點背景色
event.target.style.background = '';
// 將被拖拉節(jié)點插入目標節(jié)點
dragged.parentNode.removeChild(dragged);
event.target.appendChild( dragged );
}
}, false);
關于拖拉事件,有以下幾個注意點。
- 拖拉過程只觸發(fā)以上這些拖拉事件,盡管鼠標在移動,但是鼠標事件不會觸發(fā)。
- 將文件從操作系統(tǒng)拖拉進瀏覽器,不會觸發(fā)
dragstart
和dragend
事件。 dragenter
和dragover
事件的監(jiān)聽函數(shù),用來取出拖拉的數(shù)據(jù)(即允許放下被拖拉的元素)。由于網(wǎng)頁的大部分區(qū)域不適合作為放下拖拉元素的目標節(jié)點,所以這兩個事件的默認設置為當前節(jié)點不允許接受被拖拉的元素。如果想要在目標節(jié)點上放下的數(shù)據(jù),首先必須阻止這兩個事件的默認行為。
<div ondragover="return false">
<div ondragover="event.preventDefault()">
上面代碼中,如果不取消拖拉事件或者阻止默認行為,就不能在div
節(jié)點上放下被拖拉的節(jié)點。
DragEvent 接口
拖拉事件都繼承了DragEvent
接口,這個接口又繼承了MouseEvent
接口和Event
接口。
瀏覽器原生提供一個DragEvent()
構造函數(shù),用來生成拖拉事件的實例對象。
new DragEvent(type, options)
DragEvent()
構造函數(shù)接受兩個參數(shù),第一個參數(shù)是字符串,表示事件的類型,該參數(shù)必須;第二個參數(shù)是事件的配置對象,用來設置事件的屬性,該參數(shù)可選。配置對象除了接受MouseEvent
接口和Event
接口的配置屬性,還可以設置dataTransfer
屬性要么是null
,要么是一個DataTransfer
接口的實例。
DataTransfer
的實例對象用來讀寫拖拉事件中傳輸?shù)臄?shù)據(jù),詳見下文《DataTransfer 接口》的部分。
DataTransfer 接口概述
所有拖拉事件的實例都有一個DragEvent.dataTransfer
屬性,用來讀寫需要傳遞的數(shù)據(jù)。這個屬性的值是一個DataTransfer
接口的實例。
瀏覽器原生提供一個DataTransfer()
構造函數(shù),用來生成DataTransfer
實例對象。
var dataTrans = new DataTransfer();
DataTransfer()
構造函數(shù)不接受參數(shù)。
拖拉的數(shù)據(jù)分成兩方面:數(shù)據(jù)的種類(又稱格式)和數(shù)據(jù)的值。數(shù)據(jù)的種類是一個 MIME 字符串(比如text/plain
、image/jpeg
),數(shù)據(jù)的值是一個字符串。一般來說,如果拖拉一段文本,則數(shù)據(jù)默認就是那段文本;如果拖拉一個鏈接,則數(shù)據(jù)默認就是鏈接的 URL。
拖拉事件開始時,開發(fā)者可以提供數(shù)據(jù)類型和數(shù)據(jù)值。拖拉過程中,開發(fā)者通過dragenter
和dragover
事件的監(jiān)聽函數(shù),檢查數(shù)據(jù)類型,以確定是否允許放下(drop)被拖拉的對象。比如,在只允許放下鏈接的區(qū)域,檢查拖拉的數(shù)據(jù)類型是否為text/uri-list
。
發(fā)生drop
事件時,監(jiān)聽函數(shù)取出拖拉的數(shù)據(jù),對其進行處理。
DataTransfer 的實例屬性
DataTransfer.dropEffect
DataTransfer.dropEffect
屬性用來設置放下(drop)被拖拉節(jié)點時的效果,會影響到拖拉經(jīng)過相關區(qū)域時鼠標的形狀。它可能取下面的值。
- copy:復制被拖拉的節(jié)點
- move:移動被拖拉的節(jié)點
- link:創(chuàng)建指向被拖拉的節(jié)點的鏈接
- none:無法放下被拖拉的節(jié)點
除了上面這些值,設置其他的值都是無效的。
target.addEventListener('dragover', function (e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
});
上面代碼中,被拖拉元素一旦drop
,接受的區(qū)域會復制該節(jié)點。
dropEffect
屬性一般在dragenter
和dragover
事件的監(jiān)聽函數(shù)中設置,對于dragstart
、drag
、dragleave
這三個事件,該屬性不起作用。因為該屬性只對接受被拖拉的節(jié)點的區(qū)域有效,對被拖拉的節(jié)點本身是無效的。進入目標區(qū)域后,拖拉行為會初始化成設定的效果。
DataTransfer.effectAllowed
DataTransfer.effectAllowed
屬性設置本次拖拉中允許的效果。它可能取下面的值。
- copy:復制被拖拉的節(jié)點
- move:移動被拖拉的節(jié)點
- link:創(chuàng)建指向被拖拉節(jié)點的鏈接
- copyLink:允許
copy
或link
- copyMove:允許
copy
或move
- linkMove:允許
link
或move
- all:允許所有效果
- none:無法放下被拖拉的節(jié)點
- uninitialized:默認值,等同于
all
如果某種效果是不允許的,用戶就無法在目標節(jié)點中達成這種效果。
這個屬性與dropEffect
屬性是同一件事的兩個方面。前者設置被拖拉的節(jié)點允許的效果,后者設置接受拖拉的區(qū)域的效果,它們往往配合使用。
dragstart
事件的監(jiān)聽函數(shù),可以用來設置這個屬性。其他事件的監(jiān)聽函數(shù)里面設置這個屬性是無效的。
source.addEventListener('dragstart', function (e) {
e.dataTransfer.effectAllowed = 'move';
});
target.addEventListener('dragover', function (e) {
e.dataTransfer.dropEffect = 'move';
});
只要dropEffect
屬性和effectAllowed
屬性之中,有一個為none
,就無法在目標節(jié)點上完成drop
操作。
DataTransfer.files
DataTransfer.files
屬性是一個 FileList 對象,包含一組本地文件,可以用來在拖拉操作中傳送。如果本次拖拉不涉及文件,則該屬性為空的 FileList 對象。
下面就是一個接收拖拉文件的例子。
// HTML 代碼如下
// <div id="output" style="min-height: 200px;border: 1px solid black;">
// 文件拖拉到這里
// </div>
var div = document.getElementById('output');
div.addEventListener("dragenter", function( event ) {
div.textContent = '';
event.stopPropagation();
event.preventDefault();
}, false);
div.addEventListener("dragover", function( event ) {
event.stopPropagation();
event.preventDefault();
}, false);
div.addEventListener("drop", function( event ) {
event.stopPropagation();
event.preventDefault();
var files = event.dataTransfer.files;
for (var i = 0; i < files.length; i++) {
div.textContent += files[i].name + ' ' + files[i].size + '字節(jié)\n';
}
}, false);
上面代碼中,通過dataTransfer.files
屬性讀取被拖拉的文件的信息。如果想要讀取文件內(nèi)容,就要使用FileReader
對象。
div.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
var fileList = e.dataTransfer.files;
if (fileList.length > 0) {
var file = fileList[0];
var reader = new FileReader();
reader.onloadend = function(e) {
if (e.target.readyState === FileReader.DONE) {
var content = reader.result;
div.innerHTML = 'File: ' + file.name + '\n\n' + content;
}
}
reader.readAsBinaryString(file);
}
});
DataTransfer.types
DataTransfer.types
屬性是一個只讀的數(shù)組,每個成員是一個字符串,里面是拖拉的數(shù)據(jù)格式(通常是 MIME 值)。比如,如果拖拉的是文字,對應的成員就是text/plain
。
下面是一個例子,通過檢查dataTransfer
屬性的類型,決定是否允許在當前節(jié)點執(zhí)行drop
操作。
function contains(list, value){
for (var i = 0; i < list.length; ++i) {
if(list[i] === value) return true;
}
return false;
}
function doDragOver(event) {
var isLink = contains(event.dataTransfer.types, 'text/uri-list');
if (isLink) event.preventDefault();
}
上面代碼中,只有當被拖拉的節(jié)點有一個是鏈接時,才允許在當前節(jié)點放下。
DataTransfer.items
DataTransfer.items
屬性返回一個類似數(shù)組的只讀對象(DataTransferItemList 實例),每個成員就是本次拖拉的一個對象(DataTransferItem 實例)。如果本次拖拉不包含對象,則返回一個空對象。
DataTransferItemList 實例具有以下的屬性和方法。
length
:返回成員的數(shù)量add(data, type)
:增加一個指定內(nèi)容和類型(比如text/html
和text/plain
)的字符串作為成員add(file)
:add
方法的另一種用法,增加一個文件作為成員remove(index)
:移除指定位置的成員clear()
:移除所有的成員
DataTransferItem 實例具有以下的屬性和方法。
kind
:返回成員的種類(string
還是file
)。type
:返回成員的類型(通常是 MIME 值)。getAsFile()
:如果被拖拉是文件,返回該文件,否則返回null
。getAsString(callback)
:如果被拖拉的是字符串,將該字符傳入指定的回調(diào)函數(shù)處理。該方法是異步的,所以需要傳入回調(diào)函數(shù)。
下面是一個例子。
div.addEventListener('drop', function (e) {
e.preventDefault();
if (e.dataTransfer.items != null) {
for (var i = 0; i < e.dataTransfer.items.length; i++) {
console.log(e.dataTransfer.items[i].kind + ': ' + e.dataTransfer.items[i].type);
}
}
});
DataTransfer 的實例方法
DataTransfer.setData()
DataTransfer.setData()
方法用來設置拖拉事件所帶有的數(shù)據(jù)。該方法沒有返回值。
event.dataTransfer.setData('text/plain', 'Text to drag');
上面代碼為當前的拖拉事件加入純文本數(shù)據(jù)。
該方法接受兩個參數(shù),都是字符串。第一個參數(shù)表示數(shù)據(jù)類型(比如text/plain
),第二個參數(shù)是具體數(shù)據(jù)。如果指定類型的數(shù)據(jù)在dataTransfer
屬性不存在,那么這些數(shù)據(jù)將被加入,否則原有的數(shù)據(jù)將被新數(shù)據(jù)替換。
如果是拖拉文本框或者拖拉選中的文本,會默認將對應的文本數(shù)據(jù),添加到dataTransfer
屬性,不用手動指定。
<div draggable="true">
aaa
</div>
上面代碼中,拖拉這個<div>
元素會自動帶上文本數(shù)據(jù)aaa
。
使用setData
方法,可以替換到原有數(shù)據(jù)。
<div
draggable="true"
ondragstart="event.dataTransfer.setData('text/plain', 'bbb')"
>
aaa
</div>
上面代碼中,拖拉數(shù)據(jù)實際上是bbb
,而不是aaa
。
下面是添加其他類型的數(shù)據(jù)。由于text/plain
是最普遍支持的格式,為了保證兼容性,建議最后總是保存一份純文本格式的數(shù)據(jù)。
var dt = event.dataTransfer;
// 添加鏈接
dt.setData('text/uri-list', 'http://www.example.com');
dt.setData('text/plain', 'http://www.example.com');
// 添加 HTML 代碼
dt.setData('text/html', 'Hello there, <strong>stranger</strong>');
dt.setData('text/plain', 'Hello there, <strong>stranger</strong>');
// 添加圖像的 URL
dt.setData('text/uri-list', imageurl);
dt.setData('text/plain', imageurl);
可以一次提供多種格式的數(shù)據(jù)。
var dt = event.dataTransfer;
dt.setData('application/x-bookmark', bookmarkString);
dt.setData('text/uri-list', 'http://www.example.com');
dt.setData('text/plain', 'http://www.example.com');
上面代碼中,通過在同一個事件上面,存放三種類型的數(shù)據(jù),使得拖拉事件可以在不同的對象上面,drop
不同的值。注意,第一種格式是一個自定義格式,瀏覽器默認無法讀取,這意味著,只有某個部署了特定代碼的節(jié)點,才可能drop
(讀取到)這個數(shù)據(jù)。
DataTransfer.getData()
DataTransfer.getData()
方法接受一個字符串(表示數(shù)據(jù)類型)作為參數(shù),返回事件所帶的指定類型的數(shù)據(jù)(通常是用setData
方法添加的數(shù)據(jù))。如果指定類型的數(shù)據(jù)不存在,則返回空字符串。通常只有drop
事件觸發(fā)后,才能取出數(shù)據(jù)。
下面是一個drop
事件的監(jiān)聽函數(shù),用來取出指定類型的數(shù)據(jù)。
function onDrop(event) {
var data = event.dataTransfer.getData('text/plain');
event.target.textContent = data;
event.preventDefault();
}
上面代碼取出拖拉事件的文本數(shù)據(jù),將其替換成當前節(jié)點的文本內(nèi)容。注意,這時還必須取消瀏覽器的默認行為,因為假如用戶拖拉的是一個鏈接,瀏覽器默認會在當前窗口打開這個鏈接。
getData
方法返回的是一個字符串,如果其中包含多項數(shù)據(jù),就必須手動解析。
function doDrop(event) {
var lines = event.dataTransfer.getData('text/uri-list').split('\n');
for (let line of lines) {
let link = document.createElement('a');
link.href = line;
link.textContent = line;
event.target.appendChild(link);
}
event.preventDefault();
}
上面代碼中,getData
方法返回的是一組鏈接,就必須自行解析。
類型值指定為URL
,可以取出第一個有效鏈接。
var link = event.dataTransfer.getData('URL');
下面的例子是從多種類型的數(shù)據(jù)里面取出數(shù)據(jù)。
function doDrop(event) {
var types = event.dataTransfer.types;
var supportedTypes = ['text/uri-list', 'text/plain'];
types = supportedTypes.filter(function (value) { types.includes(value) });
if (types.length) {
var data = event.dataTransfer.getData(types[0]);
}
event.preventDefault();
}
DataTransfer.clearData()
DataTransfer.clearData()
方法接受一個字符串(表示數(shù)據(jù)類型)作為參數(shù),刪除事件所帶的指定類型的數(shù)據(jù)。如果沒有指定類型,則刪除所有數(shù)據(jù)。如果指定類型不存在,則調(diào)用該方法不會產(chǎn)生任何效果。
event.dataTransfer.clearData('text/uri-list');
上面代碼清除事件所帶的text/uri-list
類型的數(shù)據(jù)。
該方法不會移除拖拉的文件,因此調(diào)用該方法后,DataTransfer.types
屬性可能依然會返回Files
類型(前提是存在文件拖拉)。
注意,該方法只能在dragstart
事件的監(jiān)聽函數(shù)之中使用,因為這是拖拉操作的數(shù)據(jù)唯一可寫的時機。
DataTransfer.setDragImage()
拖動過程中(dragstart
事件觸發(fā)后),瀏覽器會顯示一張圖片跟隨鼠標一起移動,表示被拖動的節(jié)點。這張圖片是自動創(chuàng)造的,通常顯示為被拖動節(jié)點的外觀,不需要自己動手設置。
DataTransfer.setDragImage()
方法可以自定義這張圖片。它接受三個參數(shù)。第一個是<img>
節(jié)點或者<canvas>
節(jié)點,如果省略或為null
,則使用被拖動的節(jié)點的外觀;第二個和第三個參數(shù)為鼠標相對于該圖片左上角的橫坐標和縱坐標。
下面是一個例子。
/* HTML 代碼如下
<div id="drag-with-image" class="dragdemo" draggable="true">
drag me
</div>
*/
var div = document.getElementById('drag-with-image');
div.addEventListener('dragstart', function (e) {
var img = document.createElement('img');
img.src = 'http://path/to/img';
e.dataTransfer.setDragImage(img, 0, 0);
}, false);
更多建議: