HTML5魔法堂:全面理解Drag & Drop API
一、前言
在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特征检测,其他基本相似
欢迎添加我的公众号一起深入探讨技术手艺人的那些事!
如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!