【详】JS实现拖拽元素互换位置

6次阅读

共计 6432 个字符,预计需要花费 17 分钟才能阅读完成。

写在前面的废话
大家好,我是练习 js 时长接近两年半的个人练习生 – 李大雷
算了,直接 鸡, 你太美~
应用场景
很多时候,我们需要让用户来自定义自己想要的菜单顺序,或者一些按钮的排序,那么这个时候,怎么给用户自定义顺序呢?拖拽无疑是最简单易懂的,因为玩过手机的都知道怎么拖动桌面的 app 来改变位置。
那么要怎么做呢?最简单的方式肯定是用 H5 的拖放啦~
一些你需要了解的基础知识
首先我们先来看看,这两个单词,drag– 拖,drop– 放,从这里就很容易看出来,这里的操作逻辑了。我们来看看有哪些事件可以给我们使用。
被我们拖的元素(按住鼠标)

ondragstart – 用户开始拖动元素时触发

ondrag – 元素正在拖动时触发

ondragend – 用户完成元素拖动后触发

释放拖拽元素时触发的事件(松开鼠标)

ondragenter – 当被鼠标拖动的对象进入其容器范围内时触发此事件

ondragover – 当某被拖动的对象在另一对象容器范围内拖动时触发此事件

ondragleave – 当被鼠标拖动的对象离开其容器范围内时触发此事件

ondrop – 在一个拖动过程中,释放鼠标键时触发此事件

我们来举例子说明一下
<div class=”A” draggable=”true” ondragstart=”fn()” ondrag=”fn()” ondragend=”fn()”></div>
<div class=”B” ondragenter=”fn()” ondragover=”fn()” ondragleave=”fn()” ondrop=”fn()”></div>
假设有 div A 和 div B,当我按住 A,开始拖动 (A dragstart 触发一次)(drag 在你移动的时候不断触发), 然后你经过了 B(B 触发了 dragenter 事件),然后你在 B 里疯狂摩擦(那就疯狂触发 B 的 dragover, 这句话怎么越读越不对劲?),然后你从 B 中出来(那就触发了 B 的 dragleave),然后又进入 B 中(并且放开鼠标,那么就会触发 B 的 drop 和 A 的 dragend);
对于 A 来说,它的事件就前面 3 个,对于 B 来说,它的事件就是后面 4 个;A 是攻,那么 B 就是受了。当然你也可以自攻自受,就像孟德尔的自交豌豆一样我们下面做的拖拽也是自攻自受的情况,因为你可能拖动 A 和 B 交换,也可能拖动 B 来和 A 交换位置。
一些需要注意的点:

如果只需要拖动外层 div,请务必把子元素的 draggable 属性设置为 false(如果子元素里面有默认可拖动元素,则需要把里面的可拖动元素的属性设置为 false);不然会引起很多奇怪的现象(比如你想拖一个包含图片的 div,结果只把图片拖出来了);
链接和图片是默认可以拖动的;
ondragenter 和 ondragleave 可能会触发多次,如果你把 A 拖动到 B 里,B 一个大 div 设置了 enter 和 leave 事件,但是它里面还有很多子 div,那么每进出一个子 div,都会触发一次 enter 和 leave 事件。

开始操刀
这个标题的 cao 是第一声。经过我们上面的一顿基础知识学习以后呢,我们就很容易想清楚这个实现逻辑。
把 A 设置为可以拖动,当 A 拖动到 B 的时候,我们就互换 A 和 B 两个 dom 节点。
至于怎么互换呢?我们可以直接调换两个节点的内容,或者我们调换两个 dom 节点的位置两种方法,这里我用的是第一种方法,第二种留给大家去尝试啦~
1. 我们先写一个大概的样式

2. html 结构如下
<div class=”card” draggable=”true”
ondrag=”handleDrag(event,this)”
ondragstart=”handleDragStart(event,this)”
ondragover=”handleDragOver(event,this)”
ondragend=”handleDragEnd(event,this)”
ondrop=”handleDrop(event,this)”
ondragenter=”handleDragEnter(event,this)”>

<span class=”card-name”>
${title}
</span>
<div class=”card-img”>
<img src=”${src}” draggable=”false” alt=””>
</div>
</div>
3. 开始写逻辑,请仔细查看注释
// 先定义两个变量来保存源元素,以及目标元素,还有记录一下上次交换的 dom
// 为什么要这一步呢?往后面看
let fromDom = null,
toDom = null,
lastDom = null;

// 开始拖拽
function handleDragStart(e, dom) {
// 开始拖拽的时候,把来源保存下来
fromDom = dom;
}
// 拖拽中
function handleDrag(){
console.log(‘ 如果你有业务逻辑的话,你可以写,但是我没有,抱歉 ’)
}
// 拖到了另一个 div 中,这个时候的 dom 就是另一个元素了哦
function handleDragEnter(e, dom) {
// 保存目标元素
toDom = dom;
if(fromDom == lastDom){
// 第一次调换
// 为什么要分为几次调换位置呢?
// 想一下,如果我刚 A 和 B 调换了位置,那么就是 B 和 A 了但是此时我的鼠标还没有松开!
// 那么我又移动到 C,那么互换的位置就是 B 和 C 了,但是其实我一开始拖拽的是 A,我只想换 AC 只是不小心路过了 B!
// 因此我们这里就要使用一个 lastDom 来记录上次路过交换的 DOM,同时也要区分第几次调换。
swapDom(lastDom, toDom);
// 记录新的‘上一个 dom’
lastDom = toDom;
}else{
// 这个防止 enter 多次触发
if(lastDom == toDom){return;}
// 第 N + 1 次调换,要先把上一个 div 的东西还原回去,再跟第三个 div 互换
swapDom(fromDom,lastDom);
swapDom(fromDom,toDom);
// 记录新的‘上一个 dom’
lastDom = toDom;
}
}

// 在 B 中移动
function handleDragOver(e, dom) {
// 默认无法把元素放置到其他元素当中,如果这个不写,无法交换 div 的 innerHTML 值,所以需要阻止默认事件,这一步很重要!!
e.preventDefault();
}

// 放手
function handleDragEnd(e,dom){
// 拖拽时松开鼠标就会会触发 dragend 事件,这个 dom 是拖拽的节点。
// 重置 toDom,下次拖拽就是新拖拽了,fromDom 和 lastDom 会在 dragStart 的时候重置
toDom = null;
}
// 有上面那个,其实这个可以省略了。
function handleDrop(e, dom) {
// 只有在可放置的元素上面松开鼠标才会触发 drop 事件,所以这个 dom 是被放置的 dom 节点。
// 重置 toDom,下次拖拽就是新拖拽了,fromDom 和 lastDom 会在 dragStart 的时候重置
toDom = null;
}

// 交换 dom 内容
function swapDom(from, to) {
let temp = a.innerHTML;
a.innerHTML = b.innerHTML;
b.innerHTML = temp;
}
总结
其实我们用不上那么多事件回调,主要的是 开始拖拽保存来源,进入目标时,保存目标,并且经过判断后交换,交换完以后,我们就把目标重置,完事~ 逻辑比较简单,不过写动态 css 比较麻烦(因为我们需要一些 css 的效果来分辨哪个是被你拖动的,那个又互换了位置之类的,有比较好的用户体验),刚开始写经常傻傻分不清是来源 dom 还是目标 dom~
辣鸡源码
此部分适合新手玩家,因为自己只是随意写写,并没有写得很规范,希望大家不要学习!
<!DOCTYPE html>
<html>

<head>
<title></title>
<style>
body {
margin: 0;
}

.box {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}

.card {
flex: 1;
min-width: 26%;
max-width: calc(33.3% – 40px);
height: 200px;
margin: 30px 10px;
position: relative;
padding: 10px;
box-shadow: 0 2px 5px 0 #999;
border-radius: 5px;
border: 2px dashed transparent;
}

.card-name {
position: absolute;
top: 10px;
left: 10px;
line-height: 20px;
height: 20px;
}

.card-img {
position: relative;
padding-top: 20px;
box-sizing: border-box;
width: 100%;
height: 100%;
overflow: hidden;
}
.card-img img {
width: 100%;
height: 100%;
}

.dragging-over * {
pointer-events: none;
}
</style>
</head>

<body>
<div class=”box”>
</div>
</body>
<script>
const htmlArr = [
{title: ‘ 示例 1 - 风景 ’, src: ‘https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2756575517,833879878&fm=200&gp=0.jpg’},
{title: ‘ 示例 2 - 风景 ’, src: ‘https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=328517395,2303970886&fm=26&gp=0.jpg’},
{title: ‘ 示例 3 - 风景 ’, src: ‘https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1554369684535&di=1c1dbfbd4545ad0a05e12cbbbfe3eeef&imgtype=0&src=http%3A%2F%2Fpic41.nipic.com%2F20140601%2F18681759_143805185000_2.jpg’},
{title: ‘ 示例 4 - 风景 ’, src: ‘https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1554369684534&di=d6e34af6fce6564f9df6c4eecc27d2ce&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fimgad%2Fpic%2Fitem%2F4d086e061d950a7b9138ff1000d162d9f3d3c9d1.jpg’},
]

let fromDom = null,
toDom = null,
lastDom = null;

function handleDragStart(e, dom) {
lastDom = fromDom = dom;
dom.style.border = “2px dashed #999”;
dom.style.opacity = 0.4;
}

function handleDrop(e, dom) {
// 只有在可放置的元素上面松开鼠标才会触发 drop 事件
console.log(‘drop’);
dom.style.opacity = “”;
fromDom = null;
toDom = null;
}
function handleDragEnd(e,dom){
// 拖拽时松开鼠标就会会触发 dragend 事件
console.log(‘end’);
dom.style.border = “2px dashed transparent”;
dom.style.opacity = “”;
toDom = null;
}
function handleDragEnter(e, dom) {
toDom = dom;
if(fromDom == lastDom){
// 第一次调换
swapDom(lastDom, toDom);
lastDom = toDom;
}else{
// 第 N + 1 次调换,要先把上一个 div 的东西还原回去,再跟第三个 div 互换
// 这个防止 enter 多次触发
if(lastDom == toDom){return;}
swapDom(fromDom,lastDom);
swapDom(fromDom,toDom);
lastDom = toDom;
}
}
function handleDragOver(e, dom) {
// 默认无法把元素放置到其他元素当中,所以需要 prevent
e.preventDefault();
e.dataTransfer.effectAllowed = “move”;
}

function swapDom(a, b) {
// a 和 b 的 innerHTML 互换
let temp = a.innerHTML;
a.innerHTML = b.innerHTML;
b.innerHTML = temp;
}

// 生成 dom 结构
function createDom(arr) {
let body = document.getElementsByClassName(‘box’)[0];
let html = [];
for (let i = 0, len = arr.length; i < len; i++) {
html.push(template(arr[i].title, arr[i].src));
}
body.innerHTML = html.join(”);
}

//html 模板,根据该模板动态生成 dom 节点
function template(title, src) {
let tpl = `<div class=”card” draggable=”true” ondragstart=”handleDragStart(event,this)” ondragover=”handleDragOver(event,this)” ondragend=”handleDragEnd(event,this)” ondrop=”handleDrop(event,this)” ondragenter=”handleDragEnter(event,this)”>
<span class=”card-name”>
${title}
</span>
<div class=”card-img”>
<img src=”${src}” draggable=”false” alt=””>
</div>
</div>`
return tpl;
}
window.onload = function() {
createDom(htmlArr);
}
</script>

</html>

一些你可能不感兴趣的后语
其实在没有这个 drag 之前,是用鼠标事件来实现的,这里就简单讲讲思路好了,懒得写了~

注册 mousedown 事件,
在 mousedown 触发的时候注册 mousemove 事件,根据鼠标移动的位置来定位点击的 dom,也就是让这个元素跟着你的鼠标移动(你的 dom 得绝对定位哦),这里比较麻烦的就是一些边界的判定,因为你的鼠标能到边界,但是你的 div 不一定可以(div 面积比较大),而且根据业务不同,你也可能有不同的操作,这里因人而异啦~
在 mousedown 里也注册 mouseup 事件,mouseup 的作用就是把 mousemove 事件清空,因为要每一次鼠标按下去的时候才能有 mousemove 事件。
至于交换的话,上面也有说了。

谢谢大家,希望大家写代码不要像 cxk。

正文完
 0