html5 Sortable.js插件源码逐行分析

591 阅读8分钟

源码分享者:姚观寿

最近公司项目经常用到一个拖拽 Sortable.js插件,所以有空的时候看了 Sortable.js 源码,总共1300多行这样,写的挺完美的。   本帖属于原创,转载请出名出处。

girhub地址:github.com/qq281113270… 希望大家帮我在github 点个赞 支持我 由于掘金数字限制我的Sortable源码逐行贴不出来,所以大家要看去下载文件看。

技术交流qq群 302817612

拖拽的时候主要由这几个事件完成, 

    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;
}

拖拽的时候有阴影效果更好看些


    <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>

该插件还提供了拖拽时候动画,让拖拽变得更炫,很简单加多一个参数就行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>

我们看看上面的例子,首先看看当拖拽完成的时候他发生事件顺序

image.png

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


      <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>

该插件还提供了多列表拖拽。下面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要相同才能互相拖拽


    <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>

当然也支持 只能从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

    <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>


该插件也支持删除拖拽列表的节点,主要是设置filter 参数,改参数可以设置成函数,但是设置成函数的时候不还要自己定义拖拽,显得有些麻烦,所以一般设置成class,或者是tag,设置成class和tag的时候就是做拖拽列表中含有calss,tag的节点可以点击的时候可以触发onFilter函数,触发会传递一个evt参数进来evt.item 就是class或者tag的dom节点,可以通过他们的血缘关系从而删除需要删除的节点。


    <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>

其他参数接口设置:
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上
\