时间:2021-07-01 10:21:17 帮助过:8人阅读
最近公司项目经常用到一个拖拽 Sortable.js插件,所以有空的时候看了 Sortable.js 源码,总共1300多行这样,写的挺完美的。
官网: http://rubaxa.github.io/Sortable/
拖拽的时候主要由这几个事件完成,
ondragstart 事件:当拖拽元素开始被拖拽的时候触发的事件,此事件作用在被拖曳元素上
ondragenter 事件:当拖曳元素进入目标元素的时候触发的事件,此事件作用在目标元素上
ondragover 事件:拖拽元素在目标元素上移动的时候触发的事件,此事件作用在目标元素上
ondrop 事件:被拖拽的元素在目标元素上同时鼠标放开触发的事件,此事件作用在目标元素上
ondragend 事件:当拖拽完成后触发的事件,此事件作用在被拖曳元素上
主要是拖拽的时候发生ondragstart事件和ondragover事件的时候节点交换位置,其实就是把他们的节点互相调换,当然这只是最简单的拖拽排序方式,里面用到了许多技术比用判断拖拽滚动条的时候是滚动拖拽元素上面的根节点的父节点滚动,还是滚动window上面的滚动条, 还有拖拽的时候利用getBoundingClientRect() 属性判断鼠标是在dom节点的左边,右边,上面,还是下面。还有利用回调和函数式编程声明函数,利用布尔值相加相减去,做0和1判断,利用了事件绑定来判定两个列表中的不同元素,这些都是很有趣的技术。
注意:这个插件是用html5 拖拽的所以也不支持ie9 以下浏览器
接下来我们先看看简单的简单的dome,先加载他的拖拽js Sortable.js 插件,和app.css. 创建一个简单的拖拽很简单 只要传递一个dom节点进去就可以,第二个参数传一个空对象进去
当然app.css,加不加无所谓,如果不加的话要加一个样式就是
.sortable-ghost { opacity: 0.4; background-color: #F4E2C9; }
拖拽的时候有阴影效果更好看些
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>无标题文档</title> </head> <link href="app.css" rel="stylesheet" type="text/css"/> <script src="Sortable.js"></script> <body> <ul id="foo" class="block__list block__list_words"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> </ul> <script> Sortable.create(document.getElementById('foo'), {}); </script> </body> </html>
该插件还提供了拖拽时候动画,让拖拽变得更炫,很简单加多一个参数就行animation: 150,拖拽时间内执行完动画的时间。里面是用css3动画的ie9以下浏览器 含ie9浏览器不支持
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>无标题文档</title> </head> <link href="app.css" rel="stylesheet" type="text/css"/> <script src="Sortable.js"></script> <body> <ul id="foo" class="block__list block__list_words"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> </ul> <script> Sortable.create(document.getElementById('foo'), { animation: 150, //动画参数 }); </script> </body> </html>
这个插件不仅仅提供拖拽功能,还提供了拖拽完之后排序,当然排序的思维很简单,判断鼠标按下去拖拽的那个节点和拖拽到目标节点的两个元素发生ondragover的时候判断他们的dom节点位置,并且互换dom位置就形成了排序。拖拽完成只有 Sortable.js 插件还提供了几个事件接口,我们看看那dome,
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>无标题文档</title> </head> <link href="app.css" rel="stylesheet" type="text/css"/> <script src="Sortable.js"></script> <body> <ul id="foo" class="block__list block__list_words"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> </ul> <script> Sortable.create(document.getElementById('foo'), { animation: 150, //动画参数 onAdd: function (evt){ //拖拽时候添加有新的节点的时候发生该事件 console.log('onAdd.foo:', [evt.item, evt.from]); }, onUpdate: function (evt){ //拖拽更新节点位置发生该事件 console.log('onUpdate.foo:', [evt.item, evt.from]); }, onRemove: function (evt){ //删除拖拽节点的时候促发该事件 console.log('onRemove.foo:', [evt.item, evt.from]); }, onStart:function(evt){ //开始拖拽出发该函数 console.log('onStart.foo:', [evt.item, evt.from]); }, onSort:function(evt){ //发生排序发生该事件 console.log('onSort.foo:', [evt.item, evt.from]); }, onEnd: function(evt){ //拖拽完毕之后发生该事件 console.log('onEnd.foo:', [evt.item, evt.from]); } }); </script> </body> </html>
我们看看上面的例子,首先看看当拖拽完成的时候他发生事件顺序
onAdd onRemove 没有触发说明当列表中的拖拽数据有增加和减少的时候才会发生该事件, 当然如果有兴趣的朋友可以看看他生事件的顺序和条件。
还有传递一个evt参数,我们看看该参数有些什么东西。主要看_dispatchEvent 这个函数 改函数的功能是:创建一个事件,事件参数主要由name 提供,并且触发该事件,其实就是模拟事件并且触发该事件。
看看改函数的关键源码
var evt = document.createEvent('Event'), //创建一个事件 options = (sortable || rootEl[expando]).options, //获取options 参数 //name.charAt(0) 获取name的第一个字符串 //toUpperCase() 变成大写 //name.substr(1) 提取从索引为1下标到字符串的结束位置的字符串 //onName 将获得 on+首个字母大写+name从第一个下标获取到的字符串 onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); evt.initEvent(name, true, true); //自定义一个事件 evt.to = rootEl; //在触发该事件发生evt的时候,将evt添加多一个to属性,值为rootEl evt.from = fromEl || rootEl; //在触发该事件发生evt的时候,将evt添加多一个to属性,值为rootEl evt.item = targetEl || rootEl; //在触发该事件发生evt的时候,将evt添加多一个to属性,值为rootEl evt.clone = cloneEl; //在触发该事件发生evt的时候,将evt添加多一个to属性,值为rootEl evt.oldIndex = startIndex; //开始拖拽节点 evt.newIndex = newIndex; //现在节点 //触发该事件,并且是在rootEl 节点上面 。触发事件接口就这这里了。onAdd: onUpdate: onRemove:onStart:onSort:onEnd:
接下来事件有了, 我们怎么做排序呢?其实很简单,只要我们做排序的列表中加一个drag-id就可以,然后在拖拽过程中有几个事件onAdd, onUpdate,onRemove,onStart,onSort,onEnd,然后我们不需要关心这么多事件,我们也不需要关心中间拖拽发生了什么事情。然后我们关心的是当拖拽结束之后我们只要调用onEnd事件就可以了 然后改接口会提供 evt。 evt中可以有一个from就是拖列表的根节点,只要获取到改节点下面的字节的就可以获取到排序id。请看dome
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>无标题文档</title> </head> <link href="app.css" rel="stylesheet" type="text/css"/> <script src="Sortable.js"></script> <body> <ul id="foo" class="block__list block__list_words"> <li drag-id="1">1</li> <li drag-id="2">2</li> <li drag-id="3">3</li> <li drag-id="4">4</li> <li drag-id="5">5</li> <li drag-id="6">6</li> <li drag-id="7">7</li> <li drag-id="8">8</li> </ul> <script> Sortable.create(document.getElementById('foo'), { animation: 150, //动画参数 onAdd: function (evt){ //拖拽时候添加有新的节点的时候发生该事件 console.log('onAdd.foo:', [evt.item, evt.from]); }, onUpdate: function (evt){ //拖拽更新节点位置发生该事件 console.log('onUpdate.foo:', [evt.item, evt.from]); }, onRemove: function (evt){ //删除拖拽节点的时候促发该事件 console.log('onRemove.foo:', [evt.item, evt.from]); }, onStart:function(evt){ //开始拖拽出发该函数 console.log('onStart.foo:', [evt.item, evt.from]); }, onSort:function(evt){ //发生排序发生该事件 console.log('onSort.foo:', [evt.item, evt.from]); }, onEnd: function(evt){ //拖拽完毕之后发生该事件 console.log('onEnd.foo:', [evt.item, evt.from]); var id_arr='' for(var i=0, len=evt.from.children.length; i<len; i++){ id_arr+=','+ evt.from.children[i].getAttribute('drag-id'); } id_arr=id_arr.substr(1); //然后请求后台ajax 这样就完成了拖拽排序 console.log(id_arr); } }); </script> </body> </html>
该插件还提供了多列表拖拽。下面dome是 从a列表拖拽到b列表,b列表拖拽到a列表 两个俩表互相拖拽,然后主要参数是 group
如果group不是对象则变成对象,并且group对象的name就等于改group的值 并且添加多['pull', 'put'] 属性默认值是true
如果设置
group{
pull:true, 则可以拖拽到其他列表 否则反之
put:true, 则可以从其他列表中放数据到改列表,false则反之
}
pull: 'clone', 还有一个作用是克隆,就是当这个列表拖拽到其他列表的时候不会删除改列表的节点。
看看简单的列表互相拖拽dome 只要设置参数group:"words", group的name要相同才能互相拖拽
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>无标题文档</title> </head> <link href="app.css" rel="stylesheet" type="text/css"/> <script src="Sortable.js"></script> <body> <div class="container" style="height: 520px"> <div data-force="30" class="layer block" style="left: 14.5%; top: 0; width: 37%"> <div class="layer title">List A</div> <ul id="foo" class="block__list block__list_words"> <li>бегемот</li> <li>корм</li> <li>антон</li> <li>сало</li> <li>железосталь</li> <li>валик</li> <li>кровать</li> <li>краб</li> </ul> </div> <div data-force="18" class="layer block" style="left: 58%; top: 143px; width: 40%;"> <div class="layer title">List B</div> <ul id="bar" class="block__list block__list_tags"> <li>казнить</li> <li>,</li> <li>нельзя</li ><li>помиловать</li> </ul> </div> </div> <script> Sortable.create(document.getElementById('foo'), { group:"words", animation: 150, //动画参数 onAdd: function (evt){ //拖拽时候添加有新的节点的时候发生该事件 console.log('onAdd.foo:', [evt.item, evt.from]); }, onUpdate: function (evt){ //拖拽更新节点位置发生该事件 console.log('onUpdate.foo:', [evt.item, evt.from]); }, onRemove: function (evt){ //删除拖拽节点的时候促发该事件 console.log('onRemove.foo:', [evt.item, evt.from]); }, onStart:function(evt){ //开始拖拽出发该函数 console.log('onStart.foo:', [evt.item, evt.from]); }, onSort:function(evt){ //发生排序发生该事件 console.log('onSort.foo:', [evt.item, evt.from]); }, onEnd: function(evt){ //拖拽完毕之后发生该事件 console.log('onEnd.foo:', [evt.item, evt.from]); } }); Sortable.create(document.getElementById('bar'), { group:"words", animation: 150, //动画参数 onAdd: function (evt){ //拖拽时候添加有新的节点的时候发生该事件 console.log('onAdd.foo:', [evt.item, evt.from]); }, onUpdate: function (evt){ //拖拽更新节点位置发生该事件 console.log('onUpdate.foo:', [evt.item, evt.from]); }, onRemove: function (evt){ //删除拖拽节点的时候促发该事件 console.log('onRemove.foo:', [evt.item, evt.from]); }, onStart:function(evt){ //开始拖拽出发该函数 console.log('onStart.foo:', [evt.item, evt.from]); }, onSort:function(evt){ //发生排序发生该事件 console.log('onSort.foo:', [evt.item, evt.from]); }, onEnd: function(evt){ //拖拽完毕之后发生该事件 console.log('onEnd.foo:', [evt.item, evt.from]); } }); </script> </body> </html>
当然也支持 只能从a列表拖拽到b列表 dome
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>无标题文档</title> </head> <link href="app.css" rel="stylesheet" type="text/css"/> <script src="Sortable.js"></script> <body> <div class="container" style="height: 520px"> <div data-force="30" class="layer block" style="left: 14.5%; top: 0; width: 37%"> <div class="layer title">List A</div> <ul id="foo" class="block__list block__list_words"> <li>бегемот</li> <li>корм</li> <li>антон</li> <li>сало</li> <li>железосталь</li> <li>валик</li> <li>кровать</li> <li>краб</li> </ul> </div> <div data-force="18" class="layer block" style="left: 58%; top: 143px; width: 40%;"> <div class="layer title">List B</div> <ul id="bar" class="block__list block__list_tags"> <li>казнить</li> <li>,</li> <li>нельзя</li ><li>помиловать</li> </ul> </div> </div> <script> Sortable.create(document.getElementById('foo'), { group: { name:"words", pull: true, put: true }, animation: 150, //动画参数 onAdd: function (evt){ //拖拽时候添加有新的节点的时候发生该事件 console.log('onAdd.foo:', [evt.item, evt.from]); }, onUpdate: function (evt){ //拖拽更新节点位置发生该事件 console.log('onUpdate.foo:', [evt.item, evt.from]); }, onRemove: function (evt){ //删除拖拽节点的时候促发该事件 console.log('onRemove.foo:', [evt.item, evt.from]); }, onStart:function(evt){ //开始拖拽出发该函数 console.log('onStart.foo:', [evt.item, evt.from]); }, onSort:function(evt){ //发生排序发生该事件 console.log('onSort.foo:', [evt.item, evt.from]); }, onEnd: function(evt){ //拖拽完毕之后发生该事件 console.log('onEnd.foo:', [evt.item, evt.from]); } }); Sortable.create(document.getElementById('bar'), { group: { name:"words", pull: false, put: true }, animation: 150, //动画参数 onAdd: function (evt){ //拖拽时候添加有新的节点的时候发生该事件 console.log('onAdd.foo:', [evt.item, evt.from]); }, onUpdate: function (evt){ //拖拽更新节点位置发生该事件 console.log('onUpdate.foo:', [evt.item, evt.from]); }, onRemove: function (evt){ //删除拖拽节点的时候促发该事件 console.log('onRemove.foo:', [evt.item, evt.from]); }, onStart:function(evt){ //开始拖拽出发该函数 console.log('onStart.foo:', [evt.item, evt.from]); }, onSort:function(evt){ //发生排序发生该事件 console.log('onSort.foo:', [evt.item, evt.from]); }, onEnd: function(evt){ //拖拽完毕之后发生该事件 console.log('onEnd.foo:', [evt.item, evt.from]); } }); </script> </body> </html>
当然也支持克隆 从a列表可克隆dom节点拖拽添加到b俩表 只要把参数 pull: 'clone', 这样就可以了 dome
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>无标题文档</title> </head> <link href="app.css" rel="stylesheet" type="text/css"/> <script src="Sortable.js"></script> <body> <div class="container" style="height: 520px"> <div data-force="30" class="layer block" style="left: 14.5%; top: 0; width: 37%"> <div class="layer title">List A</div> <ul id="foo" class="block__list block__list_words"> <li>бегемот</li> <li>корм</li> <li>антон</li> <li>сало</li> <li>железосталь</li> <li>валик</li> <li>кровать</li> <li>краб</li> </ul> </div> <div data-force="18" class="layer block" style="left: 58%; top: 143px; width: 40%;"> <div class="layer title">List B</div> <ul id="bar" class="block__list block__list_tags"> <li>казнить</li> <li>,</li> <li>нельзя</li ><li>помиловать</li> </ul> </div> </div> <script> Sortable.create(document.getElementById('foo'), { group: { name:"words", pull: 'clone', put: true }, animation: 150, //动画参数 onAdd: function (evt){ //拖拽时候添加有新的节点的时候发生该事件 console.log('onAdd.foo:', [evt.item, evt.from]); }, onUpdate: function (evt){ //拖拽更新节点位置发生该事件 console.log('onUpdate.foo:', [evt.item, evt.from]); }, onRemove: function (evt){ //删除拖拽节点的时候促发该事件 console.log('onRemove.foo:', [evt.item, evt.from]); }, onStart:function(evt){ //开始拖拽出发该函数 console.log('onStart.foo:', [evt.item, evt.from]); }, onSort:function(evt){ //发生排序发生该事件 console.log('onSort.foo:', [evt.item, evt.from]); }, onEnd: function(evt){ //拖拽完毕之后发生该事件 console.log('onEnd.foo:', [evt.item, evt.from]); } }); Sortable.create(document.getElementById('bar'), { group: { name:"words", pull: false, put: true }, animation: 150, //动画参数 onAdd: function (evt){ //拖拽时候添加有新的节点的时候发生该事件 console.log('onAdd.foo:', [evt.item, evt.from]); }, onUpdate: function (evt){ //拖拽更新节点位置发生该事件 console.log('onUpdate.foo:', [evt.item, evt.from]); }, onRemove: function (evt){ //删除拖拽节点的时候促发该事件 console.log('onRemove.foo:', [evt.item, evt.from]); }, onStart:function(evt){ //开始拖拽出发该函数 console.log('onStart.foo:', [evt.item, evt.from]); }, onSort:function(evt){ //发生排序发生该事件 console.log('onSort.foo:', [evt.item, evt.from]); }, onEnd: function(evt){ //拖拽完毕之后发生该事件 console.log('onEnd.foo:', [evt.item, evt.from]); } }); </script> </body> </html>
该插件也支持删除拖拽列表的节点,主要是设置filter 参数,改参数可以设置成函数,但是设置成函数的时候不还要自己定义拖拽,显得有些麻烦,所以一般设置成class,或者是tag,设置成class和tag的时候就是做拖拽列表中含有calss,tag的节点可以点击的时候可以触发onFilter函数,触发会传递一个evt参数进来evt.item 就是class或者tag的dom节点,可以通过他们的血缘关系从而删除需要删除的节点。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>无标题文档</title> </head> <link href="st/app.css" rel="stylesheet" type="text/css"/> <script src="Sortable3.js"></script> <body> <body> <!-- Editable list --> <a name="e"></a> <div class="container" style="margin-top: 100px"> <div id="filter" style="margin-left: 30px"> <div><div data-force="5" class="layer title title_xl">Editable list</div></div> <div style="margin-top: -8px; margin-left: 10px" class="block__list block__list_words"> <ul id="editable"> <li>Оля<i class="js-remove">✖</i></li> <li>Владимир<i class="js-remove">✖</i></li> <li>Алина<i class="js-remove">✖</i></li> </ul> </div> </div> </div> <script> // Editable list var editableList = Sortable.create(document.getElementById('editable'), { animation: 150, filter: '.js-remove', onFilter: function (evt) { console.log(evt.item) evt.item.parentNode.parentNode.removeChild(evt.item.parentNode); } }); </script> </body> </html>
Sortable.js 接口参数还有很多个,不 一 一 做dome了列出来给大家看看,其中比较常用的是上面所说的dome参数,还有handle这个参数也常用规定哪些calss,或者tag拖拽。
var defaults = { group: Math.random(), //产生一个随机数 //产生一个随机数 //改参数是对象有三个两个参数 pull: 拉, put:放 默认都是是true pull还有一个值是: 'clone', pull: 拉, put:放 设置为false 就不能拖拽了, 如果 pull 这种为'clone'则可以重一个列表中拖拽到另一个列表并且克隆dom节点, name:是两个或者多个列表拖拽之间的通信,如果name相同则他们可以互相拖拽 sort: true, // 类型:Boolean,分类 false时候在自己的拖拽区域不能拖拽,但是可以拖拽到其他区域,true则可以做自己区域拖拽或者其他授权地方拖拽 disabled: false, //类型:Boolean 是否禁用拖拽 true 则不能拖拽 默认是true store: null, // 用来html5 存储的 改返回 拖拽的节点的唯一id handle: null, //handle 这个参数是设置该标签,或者该class可以拖拽 但是不要设置 id的节点和子节点相同的tag不然会有bug scroll: true, //类型:Boolean,设置拖拽的时候滚动条是否智能滚动。默认为真,则智能滚动,false则不智能滚动 scrollSensitivity: 30, //滚动的灵敏度,其实是拖拽离滚动边界的距离触发事件的距离边界+-30px的地方触发拖拽滚动事件, scrollSpeed: 10, //滚动速度 draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',//draggable 判断拖拽节点的父层是否是ou ul ghostClass: 'sortable-ghost', // 排序镜像class,就是当鼠标拉起拖拽节点的时候添加该class chosenClass: 'sortable-chosen', // //为拖拽的节点添加一个class 开始拖拽鼠标按下去的时候 添加该class ignore: 'a, img', //a 或者是img filter: null, //改参数可以传递一个函数,或者字符串,字符串可以是class或者tag,然后用于触发oFilter函数,这样可以用来自定义事件等 animation: 0, //拖拽动画时间戳 setData: function (dataTransfer, dragEl) { //设置拖拽传递的参数 dataTransfer.setData('Text', dragEl.textContent); }, dropBubble: false, // 发生 drop事件 拖拽的时候是否阻止事件冒泡 dragoverBubble: false, //发生 dragover 事件 拖拽的时候是否阻止事件冒泡 dataIdAttr: 'data-id', //拖拽元素的id 数组 delay: 0, //延迟拖拽时间, 其实就是鼠标按下去拖拽延迟 forceFallback: false, // 不详 fallbackClass: 'sortable-fallback', // 排序回退class fallbackOnBody: false,// 是否把拖拽镜像节点ghostEl放到body上 };
下面是Sortable.js插件源码,本人花了一些时间做了注释,有兴趣的朋友可以研究下。
如果您发现哪些地方有错误注释可以联系我的邮箱281113270@qq.com ;
/**! * Sortable * @author RubaXa <trash@rubaxa.org> * @license MIT */ (function (factory) { "use strict"; //严格模式 if (typeof define === "function" && define.amd) { //兼容 require.js 写法 define(factory); } else if (typeof module != "undefined" && typeof module.exports != "undefined") { //兼容node写法 module.exports = factory(); } else if (typeof Package !== "undefined") { Sortable = factory(); // export for Meteor.js 兼容 Meteor.js 写法 } else { /* jshint sub:true */ window["Sortable"] = factory(); //把它挂载在window下 } })(function () { "use strict"; if (typeof window == "undefined" || typeof window.document == "undefined") { //判断该js是否在window或者document 下运行 return function () { throw new Error("Sortable.js requires a window with a document"); //如果不是则抛出一个错误 }; } var i=0; var dragEl, //当前拖拽节点,开始拖拽节点,鼠标按下去的节点 parentEl, ghostEl, // 拖拽镜像节点 cloneEl, //克隆节点 rootEl, //鼠标开始按下去拖拽的根节点 nextEl, //下一个节点 scrollEl,//滚动节点 scrollParentEl, //滚动的父节点 lastEl, //根节点中的最后一个自己点 lastCSS, lastParentCSS, oldIndex, //开始拖拽节点的索引 就是鼠标按下去拖拽节点的索引 newIndex, //拖拽完之后现在节点 activeGroup, autoScroll = {}, //滚动对象用于存鼠标的xy轴 /* tapEvt 触摸对象包括x与y轴与拖拽当前节点 tapEvt = { target: dragEl, clientX: touch.clientX, clientY: touch.clientY }; */ tapEvt, touchEvt, moved, /** @const */ RSPACE = /\s+/g, //全局匹配空格 expando = 'Sortable' + (new Date).getTime(), //字符串Sortable+时间戳 win = window, //缩写win document = win.document, parseInt = win.parseInt; //draggable html5 拖拽属性 初始化的时候是true var supportDraggable = !!('draggable' in document.createElement('div')), //判断浏览器是否支持css3 这个属性pointer-events supportCssPointerEvents = (function (el) { el = document.createElement('x'); el.style.cssText = 'pointer-events:auto'; return el.style.pointerEvents === 'auto'; })(), _silent = false, //默认 abs = Math.abs, slice = [].slice, touchDragOverListeners = [], //新建一个数组 鼠标触摸拖拽数组 //_autoScroll 相当于 被一个函数付值 /* _autoScroll = function(callback,ms){ var args, _this; if (args === void 0) { args = arguments; _this = this; setTimeout(function () { if (args.length === 1) { callback.call(_this, args[0]); } else { callback.apply(_this, args); } args = void 0; }, ms); } 其实就是_autoScroll=function(参数){ 放到 _throttle 的回调函数中 function (/参数/) } }*/ /*********************************************************************************************** *函数名 :_autoScroll *函数功能描述 : 拖拽智能滚动 *函数参数 : evt: 类型:boj, 事件对象 options:类型:obj, 参数类 rootEl:类型:obj dom节点,拖拽的目标节点 *函数返回值 : viod *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ _autoScroll = _throttle( //回调函数 function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) { //每次拖拽只会调用一次该函数 //evt 是事件对象 event //options.scroll如果为真 并且rootEl 为真的时候 // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 if (rootEl && options.scroll) { var el, rect, sens = options.scrollSensitivity, //滚动灵敏度 默认是30 speed = options.scrollSpeed, //滚动速度 默认是10 x = evt.clientX, //获取鼠标在可视窗口的x值 y = evt.clientY, //获取鼠标在可视窗口的y值 winWidth = window.innerWidth, //获取可视窗口的高度和宽度 有兼容性问题 不包括滚动条 winHeight = window.innerHeight, vx, vy ; // Delect scrollEl 观察滚动节点 如果滚动的父节点scrollParentEl不等于当前的根节点的时候则 可以滚动 if (scrollParentEl !== rootEl) { scrollEl = options.scroll; //true 布尔值 scrollParentEl = rootEl; //鼠标开始按下的根节点 if (scrollEl === true) { scrollEl = rootEl; do { //判断父节点,哪个父节点出现滚动条,如果有滚动条则设置改拖拽的节点滚动条父节点 if ((scrollEl.offsetWidth < scrollEl.scrollWidth) || (scrollEl.offsetHeight < scrollEl.scrollHeight) ) { break; } /* jshint boss:true */ } while (scrollEl = scrollEl.parentNode); } } if (scrollEl) { el = scrollEl; rect = scrollEl.getBoundingClientRect(); /* var box=document.getElementById('box'); // 获取元素 alert(box.getBoundingClientRect().top); // 元素上边距离页面上边的距离 alert(box.getBoundingClientRect().right); // 元素右边距离页面左边的距离 alert(box.getBoundingClientRect().bottom); // 元素下边距离页面上边的距离 alert(box.getBoundingClientRect().left); // 元素左边距离页面左边的距离 y:y = evt.clientY, //获取鼠标在可视窗口的y值 sens: sens = options.scrollSensitivity, //滚动灵敏度 默认是30 */ //vx 与 vy 只是个布尔值判断 然后就得出一个值 /* true-true=0 true-false=1 false-false=0 false-true=-1 */ vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens); vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens); //这样判断并不是很好因为只会在边界判断事件发生,如果一开始拖拽快速超过了设置的+-sens值滚动事件将没有发生。个人感觉改成一下判断会比较好。 /* if(rect.top+sens-y>=0){ vy=-1; } else if(rect.bottom+sens-y<=0){ vy=1; }else{ vy=0; } */ } if (!(vx || vy)) { //当他等于0的时候 拖拽滚动的是window vx = (winWidth - x <= sens) - (x <= sens); vy = (winHeight - y <= sens) - (y <= sens); /* jshint expr:true */ (vx || vy) && (el = win); } if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) { autoScroll.el = el; autoScroll.vx = vx; autoScroll.vy = vy; //speed=10 滚动速度 clearInterval(autoScroll.pid); if (el) { autoScroll.pid = setInterval(function () { if (el === win) { win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed); } else { vy && (el.scrollTop += vy * speed); //设置元素滚动条的位置,每次滚动1*speed如果是0 则不会滚动 vx && (el.scrollLeft += vx * speed);//设置元素滚动条的位置 } }, 24); } } } //时间 毫秒 }, 30), /*********************************************************************************************** *函数名 :_prepareGroup *函数功能描述 : //options.group 属性变成对象 。如果group不是对象则变成对象,并且group对象的name就等于改group的值 并且添加多['pull', 'put'] 属性默认值是true 如果设置group{ pull:true, 则可以拖拽到其他列表 否则反之 put:true, 则可以从其他列表中放数据到改列表,false则反之 } pull: 'clone', 还有一个作用是克隆,就是当这个列表拖拽到其他列表的时候不会删除改列表的节点。 *函数参数 : options: 类型:boj, options 拖拽参数 *函数返回值 : viod *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ _prepareGroup = function (options) { var group = options.group; //把options.group 付值给group // 先判断他group 是否是对象,如果不是则变成对象,name是他的属性 if (!group || typeof group != 'object') { //如果当前options.group; 不存在或者不是obj则把他变成一个对象 group = options.group = {name: group}; } //判断有没有设置 'pull', 'put' 如果没有 则添加 'pull', 'put' 属性并且设置为真 ['pull', 'put'].forEach(function (key) { if (!(key in group)) { // group[key] = true; //将为group对象添加两个属性'pull', 'put' 并且为true } }); //options.group 变成对象之后join方法将匹配不到任何东西 //如果他直接是数组的话这里就是把数组的值拆分成字符串连接起来 //options.group 属性变成对象 。 options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' '; } ; /** * @class Sortable * @param {HTMLElement} el * @param {Object} [options] */ //el html dom节点 //param obj 数据对象 /*********************************************************************************************** *函数名 :Sortable *函数功能描述 : 主类,里面包含很多方法 *函数参数 : dom节点rootEl *函数返回值 : *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ function Sortable(el, options) { //判断 param 如果不是HTMLDOM 则抛出错误 if (!(el && el.nodeType && el.nodeType === 1)) { throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el); } //把dom节点存到this中 好操作 就是id 父层节点 this.el = el; // root element this.options = options = _extend({}, options); //把options初始化的数据存到this中 好操作 // Export instance //把 Sortable 类放在HTMLDOM节点的expando属性中 el[expando] = this; // Default options //初始化 defaults 数据 var defaults = { group: Math.random(), //产生一个随机数 //产生一个随机数 //改参数是对象有三个两个参数 pull: 拉, put:放 默认都是是true pull还有一个值是: 'clone', pull: 拉, put:放 设置为false 就不能拖拽了, 如果 pull 这种为'clone'则可以重一个列表中拖拽到另一个列表并且克隆dom节点, name:是两个或者多个列表拖拽之间的通信,如果name相同则他们可以互相拖拽 sort: true, // 类型:Boolean,分类 false时候在自己的拖拽区域不能拖拽,但是可以拖拽到其他区域,true则可以做自己区域拖拽或者其他授权地方拖拽 disabled: false, //类型:Boolean 是否禁用拖拽 true 则不能拖拽 默认是true store: null, // 用来html5 存储的 改返回 拖拽的节点的唯一id handle: null, //handle 这个参数是设置该标签,或者该class可以拖拽 但是不要设置 id的节点和子节点相同的tag不然会有bug scroll: true, //类型:Boolean,设置拖拽的时候滚动条是否智能滚动。默认为真,则智能滚动,false则不智能滚动 scrollSensitivity: 30, //滚动的灵敏度,其实是拖拽离滚动边界的距离触发事件的距离边界+-30px的地方触发拖拽滚动事件, scrollSpeed: 10, //滚动速度 draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',//draggable 判断拖拽节点的父层是否是ou ul ghostClass: 'sortable-ghost', // 排序镜像class,就是当鼠标拉起拖拽节点的时候添加该class chosenClass: 'sortable-chosen', // //为拖拽的节点添加一个class 开始拖拽鼠标按下去的时候 添加该class ignore: 'a, img', //a 或者是img filter: null, //改参数可以传递一个函数,或者字符串,字符串可以是class或者tag,然后用于触发oFilter函数,这样可以用来自定义事件等 animation: 0, //拖拽动画时间戳 setData: function (dataTransfer, dragEl) { //设置拖拽传递的参数 dataTransfer.setData('Text', dragEl.textContent); }, dropBubble: false, // 发生 drop事件 拖拽的时候是否阻止事件冒泡 dragoverBubble: false, //发生 dragover 事件 拖拽的时候是否阻止事件冒泡 dataIdAttr: 'data-id', //拖拽元素的id 数组 delay: 0, //延迟拖拽时间, 其实就是鼠标按下去拖拽延迟 forceFallback: false, // 不详 fallbackClass: 'sortable-fallback', // 排序回退class fallbackOnBody: false,// 是否把拖拽镜像节点ghostEl放到body上 }; // Set default options //当options类中的数据没有defaults类中的数据的时候 就把defaults类中的数据赋值给options类 for (var name in defaults) { !(name in options) && (options[name] = defaults[name]); } //把group: 变成一个对象,本来是一个属性的 _prepareGroup(options); // Bind all private methods for (var fn in this) { if (fn.charAt(0) === '_') { //如果这个 Sortable 类下的函数 开始字符串还有_下划线的就把他的this指向Sortable类 this[fn] = this[fn].bind(this); } } // Setup drag mode //forceFallback 如果是false 那么给supportDraggable 函数他,然后判断浏览器是否支持draggable 拖拽如果支持是true 否则是false this.nativeDraggable = options.forceFallback ? false : supportDraggable; // Bind events //添加事件 // 入口从这里开始 _on(el, 'mousedown', this._onTapStart); _on(el, 'touchstart', this._onTapStart); //html5 dragover 添加拖拽事件 if (this.nativeDraggable) { //传递整个类进去 _on(el, 'dragover', this); //然后会执行这个函数handleEvent _on(el, 'dragenter', this); //然后会执行这个函数handleEvent } //touchDragOverListeners 添加一个false 数据到数组里。 touchDragOverListeners.push(this._onDragOver); // Restore sorting //sort 排序函数 //store 是null 未找到get函数不知道怎么回事 可能它是属于store.js的api options.store && this.sort(options.store.get(this)); } /*********************************************************************************************** *函数名 :Sortable.prototype *函数功能描述 : 主类,的原型 *函数参数 : *函数返回值 : *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ Sortable.prototype = /** @lends Sortable.prototype */ { constructor: Sortable, //防止继承混乱,构造方法指向他的构造函数 /*********************************************************************************************** *函数名 :_onTapStart *函数功能描述 : 鼠标按下去函数,oldIndex统计目标节点与同级同胞的上节点总和 *函数参数 : viod *函数返回值 : 无 *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ _onTapStart: function (/** Event|TouchEvent */evt) { var _this = this, el = this.el, //id dom节点 options = this.options, //参数类 type = evt.type, //事件类型 touch = evt.touches && evt.touches[0], //触摸屏事件 target = (touch || evt).target, //目标节点 originalTarget = target, filter = options.filter; // null //如果是鼠标按下去事件,但是如果不是左键按下去的话,或者disabled 为假的时候 结束该程序 disabled 为fasle if (type === 'mousedown' && evt.button !== 0 || options.disabled) { return; // only left button or enabled } //draggable=/[uo]l/i.test(el.nodeName) ? 'li' : '>*', // target=el // target = _closest(target, options.draggable, el); //true // if (!target) { return; } // get the index of the dragged element within its parent //获取索引 oldIndex = _index(target, options.draggable); // Check filter+ //filter 如果是函数 但是默认值filter if (typeof filter === 'function') { if (filter.call(this, evt, target, this)) { //并且有返回值是true 的话 //触发该函数 _dispatchEvent(_this, originalTarget, 'filter', target, el, oldIndex); //则触发oFilter事件 evt.preventDefault(); //停止默认事件 return; // cancel dnd } } else if (filter) { //// JavaScript数组some()方法测试数组中的某个元素是否通过由提供的功能来实现测试 ,只要有一个真则返回真 /* 例子 if (!Array.prototype.some) { Array.prototype.some = function(fun ) { var len = this.length; if (typeof fun != "function") throw new TypeError(); var thisp = arguments[1]; for (var i = 0; i < len; i++) { if (i in this && fun.call(thisp, this[i], i, this)) return true; } return false; }; } function isBigEnough(element, index, array) { return (element >= 10); } var retval = [2, 5, 8, 1, 4].some(isBigEnough); document.write("Returned value is : " + retval ); var retval = [12, 5, 8, 1, 4].some(isBigEnough); document.write("<br />Returned value is : " + retval ); */ filter = filter.split(',').some(function (criteria) { //如果filter是字符串,则会用split 拆分成数组并且遍历他只有一个class 对的上则_closest 匹配tag和class 如果设置的filter中有calss 和拖拽元素上面的clss相同,或者tag相同,则会触发oFilter函数 criteria = _closest(originalTarget, criteria.trim(), el);//_closest if (criteria) { _dispatchEvent(_this, criteria, 'filter', target, el, oldIndex); //调用自定义事件 return true; } }); if (filter) { evt.preventDefault(); return; // cancel dnd } } //handle 存在 //originalTarget //handle 这个参数是设置该标签,或者该class可以拖拽 但是不要设置 id的节点和子节点相同的tag不然会有bug if (options.handle && !_closest(originalTarget, options.handle, el)) { return; } // Prepare `dragstart` // 到这里 this._prepareDragStart(evt, touch, target); }, /*********************************************************************************************** *函数名 :_onTapStart *函数功能描述 : 开始准备拖 *函数参数 : evt: 类型:obj,事件对象 touch: 类型:obj,触摸事件对象,判断是否是触摸事件还是鼠标事件 target: 类型:dom-obj,目标节点 *函数返回值 : 无 *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target) { //evt pc 的事件对象 //touch 移动的的事件对象 //target 目标节点 var _this = this, el = _this.el, //id节点,就是父层节点 options = _this.options, //参数类 ownerDocument = el.ownerDocument, //整个文档 dragStartFn; //声明开始拖拽函数 //target 目标节点存在 dragEl 当前拖拽的节点 并且目标节点的父节点是id的节点的时候 if (target && !dragEl && (target.parentNode === el)) { tapEvt = evt; //事件对象 rootEl = el; //拖拽的根节点 就是传进来的id那个节点 dragEl = target; //目标节点 当前的拖拽节点 鼠标按下去拖拽的节点 parentEl = dragEl.parentNode; //目标节点 当前的拖拽节点 的父节点 就是 dragEl.parentNode ==rootEl nextEl = dragEl.nextSibling; //目标节点 的下一个节点 activeGroup = options.group; //Object {name: "words", pull: true, put: true} //开始拖拽函数 dragStartFn = function () { // Delayed drag has been triggered 延迟拖动已被触发 // we can re-enable the events: touchmove/mousemove 我们可以重新启用touchmove / MouseMove事件: //解绑事件,关闭_dragStartTimer 定时器 取消dragStartFn 函数执行 _this._disableDelayedDrag(); // Make the element draggable 使元件拖动 //把当前的拖拽节点的draggable 属性设置为真,让他支持html5拖拽事件 dragEl.draggable = true; // Chosen item dragEl 目标节点 类 _this.options.chosenClass='sortable-chosen' //为拖拽的节点添加一个class _toggleClass(dragEl, _this.options.chosenClass, true); // Bind the events: dragstart/dragend 绑定事件拖曳开始dragend _this._triggerDragStart(touch); }; // Disable "draggable" ignore="a, img" options.ignore.split(',').forEach(function (criteria) { // criteria 遍历数组的当前target //criteria.trim() 去除空格 /* el.draggable //html5拖拽属性 function _disableDraggable(el) { el.draggable = false; } */ // 该函数功能是把当前拖拽对象的a和img节点的html5 拖拽属性改为false _find(dragEl, criteria.trim(), _disableDraggable); }); _on(ownerDocument, 'mouseup', _this._onDrop); //在ownerDocument 文档上面当发生鼠标抬起的时候,添加_onDrop函数 _on(ownerDocument, 'touchend', _this._onDrop);//在ownerDocument 文档上面当发生触摸抬起的时候,添加_onDrop函数 _on(ownerDocument, 'touchcancel', _this._onDrop);//在ownerDocument 文档上面当发生触摸划过抬起的时候,解绑_onDrop函数 //delay 初始值为0 if (options.delay) { /* 这里里面的程序块添加了事件只有调用_disableDelayedDrag,添加了一个定时器执行一次dragStartFn函数,这个函数又马上解绑_disableDelayedDrag事件,关闭定时器,整个思路是只让程序发生一次,并且马上解绑事件,销毁该事件。这样思维有些特别 */ // If the user moves the pointer or let go the click or touch 如果用户移动指针或单击“单击”或“触摸” // before the delay has been reached: //之前的延迟已达到 // disable the delayed drag //禁用延迟拖动 _on(ownerDocument, 'mouseup', _this._disableDelayedDrag); //当鼠标抬起的时候在文档上添加_disableDelayedDrag事件 _on(ownerDocument, 'touchend', _this._disableDelayedDrag); //触摸抬起的时候在文档上添加_disableDelayedDrag事件 _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag); //触摸划过抬起的时候在文档上添加_disableDelayedDrag事件 _on(ownerDocument, 'mousemove', _this._disableDelayedDrag); //当鼠标移动mousemove的时候在文档上添加_disableDelayedDrag事件 _on(ownerDocument, 'touchmove', _this._disableDelayedDrag); //触摸移动的时候在文档上添加_disableDelayedDrag事件 _this._dragStartTimer = setTimeout(dragStartFn, options.delay); //执行dragStartFn函数 } else { //开始拖拽 dragStartFn(); } } }, /*********************************************************************************************** *函数名 :_disableDelayedDrag *函数功能描述 : 禁用延迟拖拽 当拖拽延时的时候,把所有事件解绑,并且关闭定时器。 *函数参数 : *函数返回值 : *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ _disableDelayedDrag: function () { var ownerDocument = this.el.ownerDocument; clearTimeout(this._dragStartTimer); //关闭定时器 _off(ownerDocument, 'mouseup', this._disableDelayedDrag);//当鼠标抬起的时候在文档上解绑_disableDelayedDrag事件 _off(ownerDocument, 'touchend', this._disableDelayedDrag);//触摸抬起的时候在文档上解绑_disableDelayedDrag事件 _off(ownerDocument, 'touchcancel', this._disableDelayedDrag);//当触摸划过抬起的时候在文档上解绑_disableDelayedDrag事件 _off(ownerDocument, 'mousemove', this._disableDelayedDrag);//当鼠移动起的时候在文档上解绑_disableDelayedDrag事件 _off(ownerDocument, 'touchmove', this._disableDelayedDrag);//触摸的时候在文档上解绑_disableDelayedDrag事件 }, /*********************************************************************************************** *函数名 :_triggerDragStart *函数功能描述 : 为拖拽前做好准本,包括判断是否是触摸设备,或者pc,或者没有dragend *函数参数 : *函数返回值 : *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ _triggerDragStart: function (/** Touch */touch) { //按下去的值 if (touch) { // Touch device support 触摸设备支持 tapEvt = { target: dragEl, clientX: touch.clientX, clientY: touch.clientY }; this._onDragStart(tapEvt, 'touch'); //触摸设备 } else if (!this.nativeDraggable) { this._onDragStart(tapEvt, true); //pc设备 } else { //如果当前的html还没有设置拖拽属性则先设置拖拽属性 _on(dragEl, 'dragend', this); _on(rootEl, 'dragstart', this._onDragStart); } try { if (document.selection) { // Timeout neccessary for IE9 setTimeout(function () { document.selection.empty(); //取消选中 }); } else { window.getSelection().removeAllRanges();//取消选中 } } catch (err) { } }, _dragStarted: function () { if (rootEl && dragEl) { //如果鼠标按下去的拖拽节点存在和拖拽的根节点存在 // Apply effect //为拖拽节点添加一个class名字是'sortable-ghost' _toggleClass(dragEl, this.options.ghostClass, true); //Sortable类赋值给Sortable.active 属性 Sortable.active = this; // Drag start event //开始拖拽 并且会相应onStart 接口函数 _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, oldIndex); } }, _emulateDragOver: function () { if (touchEvt) { if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) { return; } this._lastX = touchEvt.clientX; this._lastY = touchEvt.clientY; if (!supportCssPointerEvents) { _css(ghostEl, 'display', 'none'); } var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY), parent = target, groupName = ' ' + this.options.group.name + '', i = touchDragOverListeners.length; if (parent) { do { if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) { while (i--) { touchDragOverListeners[i]({ clientX: touchEvt.clientX, clientY: touchEvt.clientY, target: target, rootEl: parent }); } break; } target = parent; // store last element } /* jshint boss:true */ while (parent = parent.parentNode); } if (!supportCssPointerEvents) { _css(ghostEl, 'display', ''); } } }, /* tapEvt = { target: dragEl, clientX: touch.clientX, clientY: touch.clientY }; */ /*********************************************************************************************** *函数名 :_onTouchMove *函数功能描述 : 触摸移动拖拽动画事件ghostEl,把拖拽移动的xy值给ghostEl节点 *函数参数 : viod *函数返回值 : 无 *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ _onTouchMove: function (/**TouchEvent*/evt) { //evt 事件对象 if (tapEvt) { // only set the status to dragging, when we are actually dragging if (!Sortable.active) { //Sortable.active 不存在则执行_dragStarted函数 设置拖拽动态 this._dragStarted(); } // as well as creating the ghost element on the document body // 创建一个ghostEl dom节点,并且是克隆拖拽节点的rootEl下面就是id那个dom节点,添加在,并且设置了一些属性,高,宽,top,left,透明度,鼠标样式, this._appendGhost(); var touch = evt.touches ? evt.touches[0] : evt, //判断是否是触摸事件还是pc鼠标事件 dx = touch.clientX - tapEvt.clientX, //鼠标移动的x位置减去鼠标按下去的位置。 dy = touch.clientY - tapEvt.clientY,//鼠标移动的y位置减去鼠标按下去的位置。 //3d 特效 x是左右,y是上下,z是放大缩小 设置3d效果 translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)'; moved = true; touchEvt = touch; //事件对象 _css(ghostEl, 'webkitTransform', translate3d); //设置3d效果 _css(ghostEl, 'mozTransform', translate3d); //设置3d效果 _css(ghostEl, 'msTransform', translate3d) ; //设置3d效果 _css(ghostEl, 'transform', translate3d); //设置3d效果 evt.preventDefault(); // 阻止默认事件 } }, /*********************************************************************************************** *函数名 :_appendGhost *函数功能描述 : 创建一个ghostEl dom节点,并且是克隆拖拽节点的rootEl下面就是id那个dom节点,添加在,并且设置了一些属性,高,宽,top,left,透明度,鼠标样式, *函数参数 : viod *函数返回值 : 无 *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ _appendGhost: function () { if (!ghostEl) { // 如果ghostEl 是空的,或者是假,或者是undefined,或者是0,则执行下面程序 /*getBoundingClientRect() 其实跟 o_dom.getBoundingClientRect().left= o_dom.offsetLeft; 他们值相等 这个方法返回一个矩形对象,包含四个属性:left、top、right和bottom。分别表示元素各边与页面上边和左边的距离。 */ var rect = dragEl.getBoundingClientRect(), css = _css(dragEl), //返回当前obj 所有的style的属性 options = this.options, //this.options 参数 ghostRect; //一个空变量 ghostEl = dragEl.cloneNode(true); //克隆dragEl 当前拖拽的节点 //options.ghostClass='sortable-ghost' _toggleClass(ghostEl, options.ghostClass, false); //fallbackClass= 'sortable-fallback _toggleClass(ghostEl, options.fallbackClass, true); //给新创建的节点的left和top和该节点的left和top值相等,所以要减去marginTop,marginLeft _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10)); _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10)); _css(ghostEl, 'width', rect.width); //宽和高和拖拽节点相同 _css(ghostEl, 'height', rect.height); _css(ghostEl, 'opacity', '0.8'); //透明度为0.8 _css(ghostEl, 'position', 'fixed'); // 固定定位 _css(ghostEl, 'zIndex', '100000'); //层为100000 _css(ghostEl, 'pointerEvents', 'none'); //pointer-events:none顾名思意,就是鼠标事件拜拜的意思。元素应用了该CSS属性,链接啊,点击啊什么的都变成了“浮云牌酱油”。 //把ghostEl 添加到拖拽的根节点那 options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl); // Fixing dimensions. 固定尺寸 但是我觉这样写多此一举,因为上面已经设置高宽了,然后再乘以2,再减去一般结果还是一样的 ghostRect = ghostEl.getBoundingClientRect(); _css(ghostEl, 'width', rect.width * 2 - ghostRect.width); _css(ghostEl, 'height', rect.height * 2 - ghostRect.height); } }, /*********************************************************************************************** *函数名 :_onDragStart *函数功能描述 : 拖拽开始 为document添加触摸事件与鼠标事件 *函数参数 : evt: 类型:obj, 事件对象 useFallback:类型:string, Boolean 值 *函数返回值 : *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) { //html5拖拽属性。dataTransfer对象有两个主要的方法:getData()方法和setData()方法。 var dataTransfer = evt.dataTransfer, options = this.options; //解绑文档上面的一些事件 this._offUpEvents(); //Object {name: "words", pull: true, put: true} //activeGroup={name: "words", pull: true, put: true} if (activeGroup.pull == 'clone') { //如果 参数是clone 则可以克隆节点而不是拖拽节点过去 cloneEl = dragEl.cloneNode(true); //cloneNode(false) 克隆复制节点,参数如果是false则不复制里面的html,true则会复制整个dom包括里面的html //设置cloneEl 节点隐藏 _css(cloneEl, 'display', 'none'); //插入加点,在当前拖拽的dom节点前面插入一个节点 rootEl.insertBefore(cloneEl, dragEl); } if (useFallback) { //如果是触摸则添加触摸事件 if (useFallback === 'touch') { // Bind touch events //添加触摸移动事件 _on(document, 'touchmove', this._onTouchMove); //添加触摸抬起事件 _on(document, 'touchend', this._onDrop); //添加触摸划过结束事件 _on(document, 'touchcancel', this._onDrop); } else { // Old brwoser //pc 添加鼠标移动事件 _on(document, 'mousemove', this._onTouchMove); //pc 添加鼠标抬起事件 _on(document, 'mouseup', this._onDrop); } this._loopId = setInterval(this._emulateDragOver, 50); } else { //html5拖拽属性。dataTransfer对象有两个主要的方法:getData()方法和setData()方法。 if (dataTransfer) { dataTransfer.effectAllowed = 'move';//move :只允许值为”move”的dropEffect。 /* setData: function (dataTransfer, dragEl) { dataTransfer.setData('Text', dragEl.textContent);} 设置拖拽时候拖拽信息 */ options.setData && options.setData.call(this, dataTransfer, dragEl); } _on(document, 'drop', this); //添加拖拽结束事件 setTimeout(this._dragStarted, 0); //pc拖拽事件 } }, /*********************************************************************************************** *函数名 :_onDragOver *函数功能描述 : 拖拽元素进进入拖拽区域, 判断拖拽节点与拖拽碰撞的节点,交换他们的dom节点位置,并执行动画。 *函数参数 :evt *函数返回值 : *作者 : *函数创建日期 : *函数修改日期 : *修改人 : *修改原因 : *版本 : *历史版本 : ***********************************************************************************************/ _onDragOver: function (/**Event*/evt) { var el = this.el, target, dragRect, revert, options = this.options, group = options.group, groupPut = group.put, isOwner = (activeGroup === group), canSort = options.sort; if (evt.preventDefault !== void 0) { evt.preventDefault(); //阻止默认事件 !options.dragoverBubble && evt.stopPropagation();//终止事件在传播过程的捕获、目标处理或起泡阶段进一步传播 } moved = true; //activeGroup={name: "words", pull: true, put: true} //activeGroup=true //options.disabled=false //isOwner=true 因为isOwner=true 则执行canSort || (revert = !rootEl.contains(dragEl)) //如果父节点包含子节点则返回true ,contains,所以当canSort 是假时候(revert = !rootEl.contains(dragEl) //revert = !rootEl.contains(dragEl) 取反赋值 //这里的if需要一个假才能拖拽 //(activeGroup.name === group.name) ==true; //(evt.rootEl === void 0 || evt.rootEl === this.el) ==true //所以 该功能是 给设置sort参数提供的 if ( activeGroup && !options.disabled && ( isOwner? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list : activeGroup.pull && groupPut && ( (activeGroup.name === group.name) || // by Name (groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array ) ) && (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback ) { // Smart auto-scrolling 智能滚动 _autoScroll(evt, options, this.el); if (_silent) { return; } target = _closest(evt.target, options.draggable, el); //调整拖拽目标节点 dragRect = dragEl.getBoundingClientRect(); //获取dom节点的一个获取left,right ,top,bottmo,值 if (revert) { //revert undefined _cloneHide(true); //设置克隆的节点隐藏还是显示 if (cloneEl || nextEl) { //如果克隆节点存在或者下一个节点存在 rootEl.insertBefore(dragEl, cloneEl || nextEl);//就把dragEl添加到克隆节点存在或者下一个节点的上面 } else if (!canSort) { //canSort 默认是true ,是设置是否 判断是否在自己区域拖拽 rootEl.appendChild(dragEl); //canSort 是假添加到根节点 } return; } //el.children.length 如果拖拽根节点没有子节点的时候该为true //el.children[0] === ghostEl如果根节点的字节点等于镜像节点的时候为真 //el === evt.target 根节点等于目标节点的时候 if ((el.children.length === 0) || (el.children[0] === ghostEl) || (el === evt.target) && (target = _ghostIsLast(el, evt)) ) { if (target) { // 如果_ghostIsLast 返回最后一个节点 if (target.animated) { //判断 target.animated 动画是否在执行,如果在执行那么就不执行下面函数 return; } targetRect = target.getBoundingClientRect(); } //隐藏克隆节点 _cloneHide(isOwner); /* rootEl:拖拽根节点 el:拖拽根节点 dragEl:拖拽节点 dragRect:拖拽几点的Rect target:目标节点或者是根节点的最后一个子节点,释放鼠标的节点 targetRect:target的Rect */ if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect) !== false) { if (!dragEl.contains(el)) { // 判断dragEl中没有存在根节点 el.appendChild(dragEl); //就把目拖拽节点添加到根节点那 parentEl = el; // actualization } //动画 this._animate(dragRect, dragEl); target && this._animate(targetRect, target); } } //target 拖拽的目标节点存在 //target.animated动画没有在执行 //target !== dragEl 拖拽的节点不等于目标节点 就是发生了dragenter事件 //target <