网络编程 
首页 > 网络编程 > 浏览文章

Sortable.js拖拽排序使用方法解析

(编辑:jimmy 日期: 2024/11/19 浏览:3 次 )

最近公司项目经常用到一个拖拽 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="/UploadFiles/2021-04-02/Sortable.js">

该插件还提供了拖拽时候动画,让拖拽变得更炫,很简单加多一个参数就行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="/UploadFiles/2021-04-02/Sortable.js">

这个插件不仅仅提供拖拽功能,还提供了拖拽完之后排序,当然排序的思维很简单,判断鼠标按下去拖拽的那个节点和拖拽到目标节点的两个元素发生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="/UploadFiles/2021-04-02/Sortable.js">

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

Sortable.js拖拽排序使用方法解析

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="/UploadFiles/2021-04-02/Sortable.js">

该插件还提供了多列表拖拽。下面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="/UploadFiles/2021-04-02/Sortable.js">
 

当然也支持 只能从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="/UploadFiles/2021-04-02/Sortable.js">

当然也支持克隆 从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="/UploadFiles/2021-04-02/Sortable.js">

该插件也支持删除拖拽列表的节点,主要是设置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="/UploadFiles/2021-04-02/Sortable3.js">
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) "mailto:281113270@qq.com">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"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 "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"拖拽我" draggable="true">列表1</div>
 
 ondragstart 事件:当拖拽元素开始被拖拽的时候触发的事件,此事件作用在被拖曳元素上
 ondragenter 事件:当拖曳元素进入目标元素的时候触发的事件,此事件作用在目标元素上
 ondragover 事件:拖拽元素在目标元素上移动的时候触发的事件,此事件作用在目标元素上
 ondrop 事件:被拖拽的元素在目标元素上同时鼠标放开触发的事件,此事件作用在目标元素上
 ondragend 事件:当拖拽完成后触发的事件,此事件作用在被拖曳元素上
 Event.preventDefault() 方法:阻止默认的些事件方法等执行。在ondragover中一定要执行preventDefault(),否则ondrop事件不会被触发。另外,如果是从其他应用软件或是文件中拖东西进来,尤其是图片的时候,默认的动作是显示这个图片或是相关信息,并不是真的执行drop。此时需要用用document的ondragover事件把它直接干掉。
 Event.effectAllowed 属性:就是拖拽的效果。
如果nativeDraggable是true 那么
 */
 if (this.nativeDraggable) {
 _off(document, 'drop', this); //解绑drop 事件 函数是handleEvent
 _off(el, 'dragstart', this._onDragStart); //解绑html5的拖拽dragstart事件 函数是 _onDragStart
 }
 //解绑文档上面的一些事件
 this._offUpEvents();

 if (evt) {
 if (moved) {
 evt.preventDefault(); //阻止默认事件
 !options.dropBubble && evt.stopPropagation(); //阻止事件冒泡
 }
 //ghostEl 在736行时候才会创建该节点,所以在736行调用_onDrop函数的时候都是为空
 //如果拖拽的镜像对象存在那么他就添加在拖拽的根节点
 ghostEl && ghostEl.parentNode.removeChild(ghostEl);

 
 if (dragEl) {
 if (this.nativeDraggable) {
 //如果拖拽节点存在了 就解绑this 的 handleEvent 事件
 _off(dragEl, 'dragend', this);
 }
 //禁用拖拽html5 属性
 _disableDraggable(dragEl);

 // Remove class's //删除css
 _toggleClass(dragEl, this.options.ghostClass, false);
 _toggleClass(dragEl, this.options.chosenClass, false);
 
 if (rootEl !== parentEl) { //如果从一个列表拖拽到另一个列表的时候
 //返回当前的索引
 newIndex = _index(dragEl, options.draggable);
 
 if (newIndex >= 0) { //如果当前的索引大于0
  // drag from one list and drop into another //从类表中拖拽到另一个列表
  //事件接口
  _dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex); //开始拖拽函数创建与触发
  
  _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);//开始拖拽函数创建与触发
 
  // Add event
  _dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex);//添加节点拖拽函数创建与触发
 
  // Remove event//删除节点拖拽函数创建与触发
  _dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex);
 }
 
 }
 else { //同一个列表中
 // Remove clone
 cloneEl && cloneEl.parentNode.removeChild(cloneEl);

 if (dragEl.nextSibling !== nextEl) {
 // Get the index of the dragged element within its parent
 newIndex = _index(dragEl, options.draggable);

 if (newIndex >= 0) {
 
  
 // drag & drop within the same list //update拖拽更新新数据
 _dispatchEvent(this, rootEl, 'update', dragEl, rootEl, oldIndex, newIndex);
 _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
 }
 }
 }

 if (Sortable.active) { //Sortable.active 存在说明已经拖拽开始了
 /* jshint eqnull:true */
 if (newIndex == null || newIndex === -1) {//newIndex 这个条件成立的时候是拖拽第一个节点并且没有更换拖拽位置
 newIndex = oldIndex;
 }
 //拖拽结束 
 _dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex);

 // Save sorting //保存排序
 this.save();
 }
 }

 }
 //重新初始化参数
 this._nulling();
 },
 /***********************************************************************************************
 *函数名 :_nulling
 *函数功能描述 : 初始化拖拽的数据
 *函数参数 :
 *函数返回值 :
 *作者 : 
 *函数创建日期 :
 *函数修改日期 :
 *修改人 :
 *修改原因 :
 *版本 :
 *历史版本 :
 ***********************************************************************************************/
 _nulling: function () {
 if (Sortable.active === this) {
 rootEl = //鼠标按下去拖拽节点的根节点
 dragEl = //鼠标按下去拖拽节点 
 parentEl = //拖拽的父节点 鼠标拖拽 发生ondragover 事件 拖拽节点放到目标节点的时候发生事件 的根节点 父节点,也有可能是鼠标按下去拖拽的根节点
 ghostEl = // 拖拽镜像
 nextEl = //下一个节点
 cloneEl = //拖拽克隆节点

 scrollEl = //滚动节点
 scrollParentEl = //滚动的父节点

 tapEvt = //tapEvt 触摸对象包括x与y轴与拖拽当前节点
 touchEvt = //触摸事件对象

 moved = //布尔值
 newIndex = //拖拽的现在索引

 lastEl = //拖拽根节点中的最后一个子节点
 lastCSS = //拖拽根节点中的最后一个子节点class

 activeGroup = //options.group
 Sortable.active = null;
 
 }
 },
 /***********************************************************************************************
 *函数名 :handleEvent
 *函数功能描述 : 为事件绑定this的时候提供该事件,判断是否在拖拽还是拖拽结束,调用对应的函数
 *函数参数 :
 evt:
 类型:object,事件类型 拖拽的事件类型
 *函数返回值 :
 *作者 : 
 *函数创建日期 :
 *函数修改日期 :
 *修改人 :
 *修改原因 :
 *版本 :
 *历史版本 :
 ***********************************************************************************************/
 handleEvent: function (/**Event*/evt) {//handleEvent 是该事件绑定这个对象的时候则发生这里的事件,则事件绑定给Sortable 的时候则发生这里的事件
 var type = evt.type;
 //dragover 在拖拽区域移动拖拽时候发生事件相当于move
 //dragenter 元素放入到拖拽的区域中相当于 over
 if (type === 'dragover' || type === 'dragenter') { //事件正在拖拽的时候 
 
 
 if (dragEl) { //在300行的时候调用dragover与dragenter事件,这个时候dragEl是处于声明而已但是没有赋值所以是undefined, 如果dragEl 存在则是真正拖拽的时候,dragEl是拖拽镜像
 this._onDragOver(evt);
 _globalDragOver(evt);
 }
 }
 else if (type === 'drop' || type === 'dragend') { //拖拽事件结束的时候
 this._onDrop(evt);
 }
 },


 /**
 * Serializes the item into an array of string.
 * @returns {String[]}
 */
 /***********************************************************************************************
 *函数名 :toArray
 
 *函数功能描述 : 获取dom节点的 data-id 的属性 如果没有则 会调用_generateId函数生成唯一表示符 
 *函数参数 : viod 
 *函数返回值 : 类型:array 生成唯一标识符的id数组
 *作者 : 
 *函数创建日期 :
 *函数修改日期 :
 *修改人 :
 *修改原因 :
 *版本 :
 *历史版本 :
 ***********************************************************************************************/
 toArray: function () {
 var order = [],
 el,
 children = this.el.children, //获取所有子节点
 i = 0,
 n = children.length, //获取子节点的长度
 options = this.options;


 for (; i < n; i++) {
 el = children[i];
 if (_closest(el, options.draggable, this.el)) {
 //getAttribute获取 data-id 的属性
 //order.push 如果没有data-id 属性获取不到值,则会调用_generateId函数生成唯一表示符
 order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
 }
 }
 //返回唯一标识符id 数组 类型
 return order;
 },


 /**
 * Sorts the elements according to the array.
 * @param {String[]} order order of the items
 */
 
 /***********************************************************************************************
 *函数名 :sort
 
 *函数功能描述 : 删除含有这个id的子节点 删除他 让他重新排序, 从栈底部插入数据
 *函数参数 : order:
  类型:array, 数组id
 
 *函数返回值 : void
 *作者 : 
 *函数创建日期 :
 *函数修改日期 :
 *修改人 :
 *修改原因 :
 *版本 :
 *历史版本 :
 ***********************************************************************************************/
 sort: function (order) {
 debugger;
 //order 数组
 var items = {},
 rootEl = this.el; //鼠标开始拖拽的根节点
 

 this.toArray().forEach(function (id, i) { //遍历this.toArray() 数组中的id
 var el = rootEl.children[i];
 
 if (_closest(el, this.options.draggable, rootEl)) {
 items[id] = el; //遍历数组中的id 赋值给一个对象
 }
 }, this);

 order.forEach(function (id) {
 if (items[id]) {
 rootEl.removeChild(items[id]); //删除含有这个id的子节点 删除他 让他重新排序,
 rootEl.appendChild(items[id]);//删除含有这个id的子节点 删除他 让他重新排序, 从栈底部插入数据
 }
 });
 },


 /**
 * Save the current sorting
 保存排序
 */
 save: function () { 
 var store = this.options.store;
 store && store.set(this);
 },


 /**
 * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
 * @param {HTMLElement} el
 * @param {String} [selector] default: `options.draggable`
 * @returns {HTMLElement|null}
 */
 /***********************************************************************************************
 *函数名 :_closest
 *函数功能描述 : 用来调节节点,匹配节点。匹配calss。 匹配触发dom该函数的dom节点中的tag或者class,selector参数可以是tag或者class或者>*,
 如果是>* 并且当前的父节点和ctx 参数相同 则不需要匹配直接返回el,如果是tag或者class则匹配。
 *函数参数 :
 el:
  类型:obj,拖拽节点dom
 selector:
  类型:字符串,如果selector是'li' : '>*'则返回是改节点dom,还有如果selector是和当前拖拽节点的name相同则也返回改节点dom,还有匹配触发该函数的el中的class是否是和参数中selector相同,相同则返回true,否则返回null
  
 *函数返回值 :dom和null
 *作者 : 
 *函数创建日期 :
 *函数修改日期 :
 *修改人 :
 *修改原因 :
 *版本 :
 *历史版本 :
 ***********************************************************************************************/
 closest: function (el, selector) {
 
 return _closest(el, selector || this.options.draggable, this.el);
 },


 /**
 * Set/get option
 * @param {string} name
 * @param {*} [value]
 * @returns {*}
 */
 /***********************************************************************************************
 *函数名 :option
 *函数功能描述 : 获取option对象中的某个参数,或者设置option对象中的某个参数
 
 *函数参数 :name:
  类型:string, option的key,
  
 value:类型:string, 设置option的值
  
 *函数返回值 : viod
 *作者 : 
 *函数创建日期 :
 *函数修改日期 :
 *修改人 :
 *修改原因 :
 *版本 :
 *历史版本 :
 ***********************************************************************************************/
 
 option: function (name, value) {
 var options = this.options;

 if (value === void 0) { //当没有传递第二个参数的时候 则返回该options参数的某个值
 return options[name];
 } else {
 options[name] = value;// 设置options参数的某个值 

 if (name === 'group') { // 如果name 是group 则在后面添加['pull', 'put']属性
 _prepareGroup(options);
 }
 }
 },


 /**
 * Destroy 破坏
 */
 /***********************************************************************************************
 *函数名 :destroy
 *函数功能描述 : 清空拖拽事件,和情况拖拽列表dom节点,销毁拖拽 。
 
 *函数参数 viod
 *函数返回值 : viod
 *作者 : 
 *函数创建日期 :
 *函数修改日期 :
 *修改人 :
 *修改原因 :
 *版本 :
 *历史版本 :
 ***********************************************************************************************/
 destroy: function () {
 var el = this.el;

 el[expando] = null; //把每一个时间戳的Sortable 的对象置为空

 _off(el, 'mousedown', this._onTapStart); // 解绑拖拽类表中的mousedown事件_onTapStart函数
 _off(el, 'touchstart', this._onTapStart); // 解绑拖拽类表中的touchstart事件_onTapStart函数

 if (this.nativeDraggable) {
 _off(el, 'dragover', this); // 解绑拖拽类表中的dragover事件handleEvent函数 
 _off(el, 'dragenter', this);// 解绑拖拽类表中的dragover事件handleEvent函数
 }

 // Remove draggable attributes
 //把页面上所有含有draggable 属性的dom节点 全部删除该属性,让它不能拖拽
 Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
 el.removeAttribute('draggable');
 });
 //删除touchDragOverListeners 触摸列表事件_onDragOver
 touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);

 this._onDrop(); //重新初始化

 this.el = el = null; //把拖拽列表的dom节点清空
 }
 };

 /***********************************************************************************************
 *函数名 :_cloneHide
 *函数功能描述 : 设置克隆的节点隐藏显示,是否添加到页面
 *函数参数 : 
 state:
  类型:Boolean 真,假
 *函数返回值 : viod
 *作者 : 
 *函数创建日期 :
 *函数修改日期 :
 *修改人 :
 *修改原因 :
 *版本 :
 *历史版本 :
 ***********************************************************************************************/
 function _cloneHide(state) {
 //state布尔值
 //cloneEl 克隆的节点
 //state 状态
 if (cloneEl && (cloneEl.state !== state)) {//如果cloneEl 存在,并且cloneEl.state 不等于state 的时候
 _css(cloneEl, 'display', state "tpl">
<img src="/UploadFiles/2021-04-02/dummy.png">

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

上一篇:jQuery 遍历map()方法详解
下一篇:jQuery图片加载显示loading效果
荣耀猎人回归!七大亮点看懂不只是轻薄本,更是游戏本的MagicBook Pro 16.
人们对于笔记本电脑有一个固有印象:要么轻薄但性能一般,要么性能强劲但笨重臃肿。然而,今年荣耀新推出的MagicBook Pro 16刷新了人们的认知——发布会上,荣耀宣布猎人游戏本正式回归,称其继承了荣耀 HUNTER 基因,并自信地为其打出“轻薄本,更是游戏本”的口号。
众所周知,寻求轻薄本的用户普遍更看重便携性、外观造型、静谧性和打字办公等用机体验,而寻求游戏本的用户则普遍更看重硬件配置、性能释放等硬核指标。把两个看似难以相干的产品融合到一起,我们不禁对它产生了强烈的好奇:作为代表荣耀猎人游戏本的跨界新物种,它究竟做了哪些平衡以兼顾不同人群的各类需求呢?