一、前言
在HTML4的時代,各前端工程師為了實現拖拽功能可說是煞費苦心,初聽HTML5的DnD API覺得那些痛苦的日子將一去不復返,但事實又是怎樣的呢?下面我們一起來看看DnD API的真面目吧!
二、由於篇幅較長,特設目錄一陀
八、DnD中最重要的數據傳遞對象──DataTransfer對象
九、[object DataTransferItemList]類型
功能:實現在div#title上按下鼠標左鍵並移動鼠標時,拖拽整個div#dialog,但釋放鼠標時停止拖拽。
html代碼片斷
<div id="dialog"> <div id="title"> Hi there! </div> <div id="content"> Welcome here every one. We would learn the HTML5 feature DnD API now! <div> </div>
js代碼片斷
DnD && DnD(document.getElementById('title'), document.getElementById('dialog'));
DnD.js工具庫

;(function(exports, contains){ var evtPrefix = '', off = 'removeEventListener', on = 'addEventListener' in document && 'addEventListener' || (evtPrefix = 'on', off = 'detachEvent') && 'attachEvent'; var docEl = document.documentElement, body = document.body; var css = function(el, prop, expectedVal){ var val; if (el.currentStyle){ val = el.currentStyle[prop]; } else{ val = window.getComputedStyle(el, null)[prop]; } if (arguments.length === 3){ if (typeof expectedVal === 'string') return val === expectedVal; if (Object.prototype.toString.call(expectedVal) === '[object RegExp]') return expectedVal.test(val); return false; } return val; }; // 修正獲取元素離頁面左上角距離的bug var props = [['marginLeft', 'marginTop'], ['borderLeft', 'borderTop'], ['left', 'top']]; var getOffsetXY = function(el){ var oxy = {ox: el.offsetLeft, oy: el.offsetTop}; if (el.offsetParent){ var poxy = getOffsetXY(el.offsetParent); oxy = {ox: oxy.ox + poxy.ox, oy: oxy.oy + poxy.oy}; } else if (css(el, 'position', /relative|absolute/i)){ for (var i = 0, prop; prop = props[i++];){ oxy = { ox: oxy.ox + (parseInt(css(el, prop[0])) || 0), oy: oxy.oy + (parseInt(css(el, prop[1])) || 0) }; } } return oxy; }; var getPointXY = function(evt){ if ('pageX' in evt){ return { x: evt.pageX, y: evt.pageY }; } else{ return { x: evt.clientX + (docEl && docEl.scrollLeft || body && body.scrollLeft || 0) - (docEl && docEl.clientLeft || body && body.clientLeft || 0), y: evt.clientY + (docEl && docEl.scrollLeft || body && body.scrollTop || 0) - (docEl && docEl.clientLeft || body && body.clientTop || 0) }; } }; exports.DnD = function(dragEl, addedEl){ if (!this instanceof DnD){ return new DnD(dragEl, addedEl); } var dragEls = []; if (contains(dragEl, addedEl)){ dragEls.push({ el: dragEl, ox: 0, oy: 0 }); } else if (contains(addedEl, dragEl)){ dragEls.push({ el: addedEl, ox: 0, oy: 0 }); } else{ dragEls = [{ el: addedEl, ox: 0, oy: 0 },{ el: dragEl, ox: 0, oy: 0 }]; } var ox, oy; dragEl[on](evtPrefix + 'mousedown', function(evt){ evt = evt || window.event; var pointXY = getPointXY(evt); ox = pointXY.x; oy = pointXY.y; for (var i = 0, currEl; currEl = dragEls[i++];){ var oxy = getOffsetXY(currEl.el); currEl.ox = oxy.ox; currEl.oy = oxy.oy; currEl.el.style.position = 'absolute'; currEl.el.style.left = currEl.ox + 'px'; currEl.el.style.top = currEl.oy + 'px'; } var onDOCMousemove = function(evt){ evt = evt || window.event; var dx = evt.clientX - ox; var dy = evt.clientY - oy; for (var i = 0, currEl; currEl = dragEls[i++];){ currEl.el.style.left = (currEl.ox + dx) + 'px'; currEl.el.style.top = (currEl.oy + dy) + 'px'; } }; document[on](evtPrefix + 'mousemove', onDOCMousemove); document[on](evtPrefix + 'mouseup', function(evt){ evt = evt || window.event; document[off](evtPrefix + 'mousemove', onDOCMousemove); }); }); }; }(window, window.contains));
contains.js工具庫

;(function(exports){ exports.contains = function(pel, cel){ // ie if (pel.contains){ return pel.contains(cel); } else if(pel.compareDocumentPosition){ return !!pel.compareDocumentPosition(cel) & 16; } else{ var p; while ((p = cel.parentNode) && p.nodeType === 1){ if (pel === p) return true; } return true; } }; }(window));
具體代碼地址:https://github.com/fsjohnhuang/DnD-polyfill/blob/master/sample/sample3/
功能:實現在div#title上按下鼠標左鍵並移動鼠標時,拖拽整個div#dialog,但釋放鼠標時停止拖拽。下面的例子僅能在FF下運行
html代碼片段
<div id="dialog"> <div id="title" draggable="true"> Hi there! </div> <div id="content"> Welcome here every one. We would learn the HTML5 feature DnD API now! <div> </div>
js代碼片段
DnD && DnD(document.getElementById('title'), document.getElementById('dialog'));
DnD.js工具庫

;(function(exports, contains){ var css = function(el, prop, expectedVal){ var val = window.getComputedStyle(el, null)[prop]; if (arguments.length === 3){ if (typeof expectedVal === 'string') return val === expectedVal; if (Object.prototype.toString.call(expectedVal) === '[object RegExp]') return expectedVal.test(val); return false; } return val; }; var props = [['marginLeft', 'marginTop'], ['borderLeft', 'borderTop'], ['left', 'top']]; var getOffsetXY = function(el){ var oxy = {ox: el.offsetLeft, oy: el.offsetTop}; if (el.offsetParent){ var poxy = getOffsetXY(el.offsetParent); oxy = {ox: oxy.ox + poxy.ox, oy: oxy.oy + poxy.oy}; } else if (css(el, 'position', /relative|absolute/i)){ for (var i = 0, prop; prop = props[i++];){ oxy = { ox: oxy.ox + (parseInt(css(el, prop[0])) || 0), oy: oxy.oy + (parseInt(css(el, prop[1])) || 0) }; } } return oxy; }; var setXY = function(e, dragEls, ox, oy){ var dx = e.clientX - ox; var dy = e.clientY - oy; dragEls.forEach(function(item, index, dragEls){ item.el.style.left = (item.ox + dx) + 'px'; item.el.style.top = (item.oy + dy) + 'px'; }); }; exports.DnD = function(dragEl, addedEl){ if (!this instanceof DnD){ return new DnD(dragEl, addedEl); } var dragEls = [], addElements = []; if (contains(dragEl, addedEl)){ dragEls.push({ el: dragEl, ox: 0, oy: 0 }); } else if (contains(addedEl, dragEl)){ dragEls.push({ el: addedEl, ox: 0, oy: 0 }); } else{ dragEls = [{ el: addedEl, ox: 0, oy: 0 },{ el: dragEl, ox: 0, oy: 0 }]; } var ox,oy; dragEl.addEventListener('dragstart', function(e){ e.dataTransfer.setData('Text', ''); e.dataTransfer.setDragImage(document.createElement('div'), 0, 0); e.dataTransfer.effectAllowed = 'move'; if (ox == null){ ox = e.pageX; oy = e.pageY; dragEls.forEach(function(item,index,dragEls){ var oxy = getOffsetXY(item.el); item.ox = oxy.ox; item.oy = oxy.oy; item.el.style.position = 'absolute'; item.el.style.left = item.ox + 'px'; item.el.style.top = item.oy + 'px'; }); // 由於dragover是在拖動一段距離后才會觸發,從而導致被拖動的元素出現突然開始移動的效果 // 通過在dragstart中修改被拖動元素offsetTop/Left可優化該情況 setXY(e, dragEls, ox, oy); } }, false); document.addEventListener('dragover', function(e){ e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setXY(e, dragEls, ox, oy); }, false); document.addEventListener('drop', function(e){ e.preventDefault(); e.stopPropagation(); }, false); }; }(window, window.contains));
contains.js工具庫與上一節的相同
具體代碼地址:https://github.com/fsjohnhuang/DnD-polyfill/blob/master/sample/sample4/
html片段
<div id="drag" draggable="true" style="width:100px;height:50px;background-color:red;"> test </div>
js片段
var drag = document.getElementById('drag'); drag.onselectstart = function(){return false;}; // FF下拖拽時,默認不會生成一個被拖拽元素的陰影並跟隨鼠標移動 // 需通過e.dataTransfer.setData來啟動該效果 drag.ondragstart = function(e){ e.dataTransfer.setData('text', e.target.innerHTML); };
關鍵點:
1. 為觸發拖拽的元素添加 draggable="true" 特性,用於啟動HTML5的DnD功能(即元素的 dragstart 事件可被觸發);
2. 在FF下即使添加 draggable="true" 特性,但僅僅會觸發 dragstart 事件,但DnD功能並沒有被完全打開(拖拽元素時沒有任何視覺效果),需要調用 event.dataTransfer.setData('Text','') 徹底開啟DnD功能。
3. 在Safari4下則需要借助CSS規則來啟動DnD功能, [draggable=true]{ -webkit-user-drag: element; }
作用:用於指定標簽是否可被拖拽
屬性值范圍如下:
1. true ,表示可被拖拽
2. false ,表示不可被拖拽
3. auto ,默認值,img和帶href屬性的a標簽則表示可拖拽,其他標簽表示不可被拖拽
4. 其他值,表示不可被拖拽
1. 被拖拽元素的生命周期
dragstart :當被拖拽元素開始被拖拽時觸發
注意:
[a]. event.dataTransfer的大部分設置均在這里配置
[b]. 若調用event.preventDefault()則會阻止拖拽行為,導致后續的拖拽事件不被觸發
[c]. 觸發dragstart事件后,其他元素的mousemove,mouseover,mouseenter,mouseleave,mouseout事件均不會被觸發了
drag :當被拖拽元素被拖拽時觸發
dragend :當拖拽行為結束后觸發
2. 目標元素的生命周期
dragenter :當被拖拽元素進入目標元素時觸發
dragover :當被拖拽元素在目標元素上移動時觸發
注意:
[a]. 可以在這里設置dropEffect的值,事件的默認行為是將dropEffect設置為none
[b]. 該事件是被拖拽元素在目標元素上移動一段時間后才觸發
[c]. 事件的默認行為是不允許被拖拽元素在其他元素上釋放或放置(即無法觸發 drop 事件),需要通過 event.preventDefault() 來阻止默認行為才能觸發后續的 drop 事件。
drop :當被拖拽元素在目標元素上,而且釋放鼠標左鍵時觸發
注意:
[a]. 對於外來的被拖拽元素(超鏈接、文件、圖片源), drop 事件的默認行為是瀏覽器將當前頁面重定向到被拖拽元素所指向的資源上
[b]. 對文檔內部的被拖拽元素,IE10+和Chrome下的默認行為是不作為,而FF得默認行為是新打開一個文檔用於訪問被拖拽元素所指向的資源
dragleave :當被拖拽元素離開目標元素時觸發。
示例代碼:
<div id="drag" style="width:50px;height:50px;background-color:red;">Test</div> <div id="drop" style="width:100px;height:100px;border:solid 1px red;"></div> <script type="text/javascript"> var drag = document.getElementById('drag'), drop = document.getElementById('drop'); drag.ondragstart = function(evt){ evt.dataTransfer.setData('Text', 'www.baidu.com'); }; drop.ondragover = function(evt){ evt.preventDefault(); // 這樣才能觸發drop的drop事件 }; </script>
3. 整體生命周期
dragstart -> drag -> dragenter -> dragover -> dragleave -> drop -> dragend
八、DnD中最重要的數據傳遞對象──DataTransfer對象
DataTransfer對象用於在配置拖拽行為效果,並且在拖拽過程的各事件間傳遞數據信息。它存儲在事件對象當中,下面我們逐步了解它吧。
1. [object DragEvent]對象
繼承自 [object MouseEvent] 對象,其實就多了個 {DataTransfer} dataTransfer 屬性
2. [object DataTransfer]對象詳解
上文說到DataTransfer對象可用於傳遞數據信息,而數據信息的數據類型被限定為字符串和文件類型
2.1. effectAllowed 和 dropEffect 屬性
這個兩個屬性對於初次接觸DnD的朋友來說,可謂最令人摸不着頭腦的,網上和各書籍上對這兩個屬性的解釋均不全面,下面我試圖盡量把它們講明白。
effectAllowed 和 dropEffect 最主要的作用是,用於配置拖拽操作過程中鼠標指針的類型以便提示用戶后續可執行怎樣的操作;其次的作用是,控制 drop 事件的觸發與否。
[a] effectAllowed
作用:用於設置被拖拽元素可執行的操作。
取值范圍:
copy ,限定dropEffect的屬性值為copy,否則會鼠標指針為禁止樣式
link ,限定dropEffect的屬性值為link,否則會鼠標指針為禁止樣式
move ,限定dropEffect的屬性值為move,否則會鼠標指針為禁止樣式
copyLink ,限定dropEffect的屬性值為copy和link,否則會鼠標指針為禁止樣式
copyMove ,限定dropEffect的屬性值為copy和move,否則會鼠標指針為禁止樣式
linkMove ,限定dropEffect的屬性值為link和move,否則會鼠標指針為禁止樣式
all ,允許dropEffect的屬性值為任意值
none ,鼠標指針一直為禁止樣式,不管dropEffect的屬性值是什么
uninitialized ,沒有限定dropEffect屬性的值,效果和 all 一樣。
注意:僅能在 dragstart 事件中設置該屬性,其他事件中設置均無效。
[b]. dropEffect
作用:用於設置目標元素將執行的操作,若屬性值屬於 effectAllowed 范圍內,則鼠標指針將顯示對應的指針樣式,否則則顯示禁止的指針樣式。
取值范圍:
copy :被拖拽元素將被復制到目標元素內,若屬於 effectAllowed 范圍內時,則鼠標指針顯示復制的樣式,否則則顯示禁止的指針樣式。
link :被拖拽元素將以超鏈接的形式打開資源(具體是否打開資源請參考七、2),若屬於 effectAllowed 范圍內時,則鼠標指針顯示超鏈接的樣式,否則則顯示禁止的指針樣式。
move :被拖拽元素將被移動到目標元素內,若屬於 effectAllowed 范圍內時,則鼠標指針顯示移動的樣式,否則則顯示禁止的指針樣式。
none :被拖拽元素不能在目標元素上作任何操作,一直顯示禁止的指針樣式。除了文本框外其他元素的默認值均為none
注意:
1. 僅能在 dragover 事件中設置該屬性值,其他事件中設置均無效
2. 當顯示禁止的指針樣式時,將無法觸發目標元素的 drop 事件。
[c]. 在真實瀏覽器中的測試結果
瀏覽器 | effectAllowed默認值 | effectAllowed值 | dropEffect默認值 |
默認使用鼠標指針的效果 |
IE10+ | uninitialized | uninitialized | copy | copy |
copyLink | none | link | ||
copyMove | none | copy | ||
linkMove | none | link | ||
all | copy | link | ||
none | ||||
move | move | move | ||
link | link | link | ||
copy | copy | copy | ||
備注: | 1. 無法通過 shift鍵 切換copyLink,copyMove和linkMove的樣式; 2. 若effectAllowed設置為copyLink、copyMove或linkMove,且dropEffect與之對應,則鼠標樣式將為dropEffect所設置的樣式 |
|||
Chrome37 | all | copyLink | none | copy |
copyMove | none | move | ||
linkMove | none | move | ||
move | move | move | ||
link | link | link | ||
copy | copy | copy | ||
all | copy | move | ||
備注: | 1. 無法通過 shift鍵 切換copyLink,copyMove和linkMove的樣式; 2. 若effectAllowed設置為copyLink、copyMove或linkMove,且dropEffect與之對應,則鼠標樣式將為dropEffect所設置的樣式 |
|||
FF31 for Windows | uninitialized | copyLink | copy | copy |
copyMove | move | move | ||
linkMove | move | move | ||
move | move | move | ||
link | link | link | ||
copy | copy | copy | ||
uninitialized | move | move | ||
備注: | 1. 可通過 shift鍵 切換copyLink,copyMove和linkMove的樣式; 2. 若effectAllowed設置為copyLink、copyMove或linkMove,且dropEffect與之對應,則鼠標樣式將為dropEffect所設置的樣式 |
|||
FF33 for Linux | 僅能觸發dragstart事件,其他事件一律無效,因此不用理會 |
2.2. 其他屬性
items :數據類型為DataTransferItemList,存儲DataTransfer對象中所有的數據項
注意:1. FF33 for Linux下沒有該屬性
2. IE10+沒有該屬性
files :數據類型為FileList(IE5~9沒有該屬性)
types :數據類型為DOMStringList,存儲DataTransfer對象中所有數據項的數據類型
注意:1. IE5~9下沒有該屬性
2. 僅能在dragenter,dragover和drop中獲取該屬性
2.3. 方法
void addElement({HTMLElement} element) :添加一起跟隨鼠標移動的元素。僅在 dragstart 事件中調用,Chrome37和IE10+不支持該方法;
void setDragImage({Element} image, {long} x, {long} y) :設置拖動時跟隨鼠標移動的圖片,用來替代默認的元素,若image不是圖片元素則會元素臨時轉換為圖片;x用於設置圖標與鼠標在水平方向上的距離,y設置圖標與鼠標在垂直方向上的距離。僅在 dragstart 事件中調用。IE10+不支持該方法;
注意:
1. {Element} image必須在DOM樹中,而且在渲染樹中(即display不為none)為有效元素,否則會導致沒有元素跟隨鼠標移動;
2. Chrome37下,若{Element} image為無效元素時,將不會觸發dragstart事件后的其他事件。
boolean setData({DOMString} format, {DOMString} data) :將指定格式的數據賦值給dataTransfer或clipboardData,format值范圍為URL、Text(或text)和各種MIME類型,其實Text會被自動映射為text/plain,URL會被自動映射為text/uri-list類型。僅在 dragstart 事件中調用。
注意:
1. FF5-是不會將text映射為text/plain,而僅僅支持Text映射為text/plain,因此使用Text或直接使用text/plain
2. IE10+僅支持Text和URL兩種類型,不支持text/plain、text/uri-list等類型
3. text/plain類型則不會對數據進行額外處理,而text/uri-list類型則會將數據視為url來使用(體現在當將元素拖拽到OS桌面釋放時)
4. Chrome和FF支持任意的非空字符串作為format
5. 當沒有填寫第二個入參時,則會根據format來刪除相應的數據項
6. 當設置text/uri-list類型的數據時,數據必須帶協議名,如http://fsjohnhuang.cnblogs.com;若僅寫為fsjohnhuang.cnblogs.com,那么dataTransfer將自動棄除,在`dragstart`事件還能獲取到,但在drop事件中將無法獲
DOMString getData({DOMString} format) :從DataTransfer對象或ClipboardData對象中獲取指定格式的數據
void clearData([{DOMString} format]) :從DataTransfer對象或ClipboardData對象中刪除指定格式或全部kind值為string的數據。僅在 dragstart 事件中調用,在其他事件中調用會拋InvalidStateError。
2.4. 數據存儲模式
Read/Write mode:在 dragstart 事件為該模式,可讀寫數據
Read-only mode:在 drop 事件為該模式,僅能讀取數據
Protected mode:在其他事件為該模式,僅能枚舉數據
九、[object DataTransferItemList]類型
readonly unsigined long length
getter DataTransferItem(unsigned long index) ,使用方式:items[1]
deleter void(unsigned long index) ,使用方式:delete items[1],在非Read/Write mode下會報InvalidStateError,而添加數據則不會有問題
void clear()
DataTransferItem add(DOMString data, DOMString type)
DataTransferItem add(File data)
readonly attribute DOMString kind ,表示數據的大類型,值范圍string和file
readonly attribute DOMString type ,表示數據的小類型,一般使用mimetype表示,但在Chrome和FF下可以是任意非空字符串
void getAsString(FunctionStringCallback cb) ,當kind為string時,則只能在Read-only mode和Read/Write mode下才可用。其中cb僅有一個類型為{DOMString}的入參
File getAsFile() ,當kind為file時,則只能在Read-only mode和Read/Write mode下才可用,沒有數據時返回null
IE5~9部分標簽支持;
IE10+、FF、Chrome支持。
也許大家會好驚訝IE5已經開始支持DnD API啦??其實DnD API的最初是由IE提出來的,只是后來被HTML5納入草案而已。大家也許會問在IE5~9上運行上文的代碼沒有效果,是不是我寫錯了,下一篇《JS魔法堂:IE5~9的Drag & Drop API(http://www.cnblogs.com/fsjohnhuang/p/3980563.html)》我們將一起探討IE5~9的DnD API。
由於IE5~9的DnD API與HTML5標准的有差異,因此特征檢測變得尤為必要了。下面的代碼參考了Modernizr.draganddrop的實現(地址:https://github.com/Modernizr/Modernizr/blob/master/feature-detects/draganddrop.js)
var supportDnD = function(){ var div = document.createElement('div'); return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div); };
回到文章最開頭的兩個示例,會發現使用HTML5 DnD API實現拖拽效果的代碼量並不比HTML4中的少,效果也並不理想(個人水平有限優化也沒做好),最讓人心酸的是各瀏覽器在細節上還是有差異的(兼容性是前端工程師一直的痛啊)。也許大家會說那么DnD API是不是就僅僅好看而不實用呢?其實不然,只是示例把這個特性用到不適合的地方而已。
HTML5 DnD API最常見的用法就是文件拖拽上傳,或把文檔內某元素拖到其他元素內或OS桌面上等。這些都是HTML4時代的js很難處理,或者無法處理的。
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/3961066.html ^_^肥仔John
http://www.w3school.com.cn/html5/html_5_draganddrop.asp
http://www.cnblogs.com/wpfpizicai/archive/2012/04/07/2436454.html
http://www.kankanews.com/ICkengine/archives/82862.shtml
http://jingyan.baidu.com/article/6dad5075cf6e62a123e36e11.html
http://www.zhangxinxu.com/wordpress/2011/02/html5-drag-drop-%E6%8B%96%E6%8B%BD%E4%B8%8E%E6%8B%96%E6%94%BE%E7%AE%80%E4%BB%8B/
http://my.oschina.net/caixw/blog/102845
http://www.cnblogs.com/birdshome/archive/2006/07/22/Drag_Drop.html
《HTML5實戰》第11章、HTML5中元素的拖放
《HTML5用戶指南》第8章、拖放
http://msdn.microsoft.com/en-us/library/ff974353(v=vs.85).aspx
《HTML5與CSS3權威指南》4.5.拖放
《論道HTML5》3.3.Drag & Drop API
《HTML5實戰》P292 setData的format參數格式包含text/url-list,應更正為text/uri-list
《HTML5實戰》第11章、HTML5中元素的拖放,這一章感覺就一筆帶過,純屬印象派。
《HTML5用戶指南》第8章、拖放,除了簡單介紹HTML5 DnD API外,還介紹起源和IE上DnD的特點和作者對DnD API不完美的抱怨,比《HTML5實戰》更值得拜讀。
《HTML5與CSS3權威指南》4.5.拖放,內容,深度與《HTML5實戰》相似
《論道HTML5》3.3.Drag & Drop API,對比上述三本書,它提及到使用Modernizr作DnD特征檢測,其他基本相似