32

前言

鲁迅说过:“如果你的技术不好,那么你的姿势要多。”

鲁迅

公司的项目最近给侧边栏加了拖动排序的效果,类似于下面这种效果(鲁迅:不是我的下面),是下面的图?:
动画

drag拖动排序

说来惭愧,上图不是我写的,不然我还能吹一波,没有看代码,但闭着眼就能猜到用的是html5的drag系列的api,相关的API可以看MDN上的介绍,我把文档拷一下做个介绍吧==

  1. drag: 当元素或者选择的文本被拖动时触发drag事件 (每几百毫秒)。
  2. dragstart: 当用户开始拖动一个元素或者一个选择文本的时候dragstart事件就会触发。一般在这个动作里面处理被拖动元素的样式。
  3. dragend: 拖动操作结束时触发,即松开鼠标操作。
  4. dragenter: a拖到b中的时候触发。
  5. dragover: 拖动过程不断触发。
  6. dragexit: 元素不是被拖动目标时。
  7. dragleave: a拖到b,不释放鼠标,再拖到c,离开b的时候触发。
  8. drop: 当一个元素或是选中的文字被拖拽释放到一个有效的释放目标位置时

上面的大部分都是对样式之类的处理,官方也给了例子,排除掉dragdragover这两个触发大户,大概的顺序是这样的:
顺序

首先,我肯定是实现了效果的,如下图所示的:
jijiDE

粗略介绍下流程吧,首先需要确定的是应该是什么效果,打个比方,我把a拖到了b上,b能很明显的感觉到,“啊,有什么奇怪的东西进入我的体内了”。但是b并不知道a是想在前面还是想在后面...这时候你就发现问题了吧,对!提问:“a和b到底谁在上面谁在下面?”所以我们刚开始要约定一个规范,把上面的a拖向下面的b,那么就把a排在b的下面,反之,把下面的a拖向上面的b,那么就把a放在b的上面~首先我需要这四个对象:

let dragObj, enterObj, dragIndex, enterIndex;
// dragObj 被拖动的对象a,整个拖动过程它是不会变的
// enterObj 最终进入的对象b,在drop里可以获取到
// dragIndex a在列表中的下标
// enterIndex b在列表的下标

通过比较dragIndexenterIndex的大小来决定a和b的关系,剩下的就是对dom进行操作了:

if (dragIndex < enterIndex) {
  dragObj.remove();
  enterObj.after(dragObj);
} else if (dragIndex > enterIndex) {
  dragObj.remove();
  enterObj.before(dragObj);
}   

是不是相当简单了效果预览 (chrome下观看)
源码

更好的实现--mouse

直接看看效果吧,也可以做个比较:
效果

预览效果
使用到的api:onmousedown, onmousemove, onmouseup,其实这些大家多多少少应该会用过,但我还是在这里粗略介绍下:

  1. onmousedown: 在当前元素上点击鼠标按键时会触发mousedown事件,有人问了,那这个和click有什么区别呢,区别是 click = mousedown + mouseup,然后是,click鼠标左键触发,mousedown鼠标点击即会触发。
  2. onmousemove: 当用户在当前元素上移动鼠标时会触发mousemove事件,就是“拖动元素。
  3. onmouseup: 当用户在当前元素上放开鼠标某个按键时会触发mouseup事件。

整个实现过程也比上面的那一种要复杂的多,不同与drag,drag自己能感知元素从a移动到了b中或者从b中离开,而鼠标事件不行。

拖动注意点

这样一个列表:
还是他们

假设我点了“小强”,“小强”被我选中,我怎么去实现拖动的效果呢?元素本身是没有拖动这个说法的,我能做到的就是动态的改变它的位置,那就是在onmousemove的时候动态的去改变了,改变元素位置且不影响到其他元素排布的,那就是absolute了。先实现拖动的效果,大概就是这样:

//在onmousedown的时候记下必要的信息,将小强的position改成absolute
const startY = event.clientY;
//在onmousemove记录下鼠标在y轴上移动的距离,横行的话就是x
const currentTop = parseInt(startTop) + (moveY - startY);

这样基本的拖动问题是解决了,元素最起码是可以拖动起来了。这里需要注意的是,只有“小强”的位置是absolute,这时候“小明”就会被小强遮住,所以我们这里需要一个元素“占位”:

<div class="item hold"><div>

--“进来了吗?” --“不是早就进来了”

各位乘客系好安全带,我要开车了...
理想的效果应该是这样的,“小强”向下拖动,距离“小明”底部小于一半时,“小明”就应该给“小强”让路,“小明”要知道小强什么时候进来了,然后hold元素向下移动一格,说明小强“此时”已经在“小明”下面了:

currentTop > itemHeight + itemHeight / 2;

记录高度实在是没必要,我只需要记录“小强” 当前 被拖动到“第几位”就可以了:

currentIndex = Math.ceil((currentTop - itemHeight / 2) / itemHeight);

只要拿到onmousedown时的“小强”的“index”,与当前的比较,只要两者不相同,就需要移动hold的位置,hold元素的位置受当前index的影响:

if (previousIndex !== currentIndex) { //... }

此处省略的代码都是dom的操作,不做太多说明。此时我们并没有对拖动的元素做限制,元素能够被拖到盒子的外面,这样肯定是不可以的,做一个限制:

if (currentIndex < 0) {
  currentIndex = 0;
} else if (currentIndex > listLength - 1) {
  currentIndex = listLength - 1;
}

这样不会限制的用户拖动的操作,我们只需要控制住index就好了,这样处理也方便的多。
在用户onmouseup的时候需要对监听器进行销毁:

document.onmousemove = null;
document.onmouseup = null;

注意事项

类似于$$querySelectorAll获取到的"数组"并不是“数组”,它有数组的部分特性,通过下图我们可以知道它是一个NodeList,并不是Array,它是有forEach方法的,但是它并没有mapfind等一系列方法,使用的时候还需注意。
list

源码

细节还是看源码吧 我在刚开始写的时候碰到很多坑,dom操作的时候更是,尤其在上下来回拖动的时候,具体的不再多讲,因为实在有点晚了。对不起,我今晚玩游戏玩到十一点半,我有罪==,洗个澡啥的,弄的很晚。
享受coding,热爱生活,拜~
coding


shiyangzhaoa
428 声望24 粉丝

立志做一个好厨师!!!