拖拽功能也算是前端里面比较常用的一种功能,我们今天来讲一下。
1.搭建场景
老规矩先搭建一个场景出来。
html
<template>
<div class="screen-view">
<div class="left">
<div class="item" v-for="item in itemList" :key="item.color" :style="{ background: item.color }"></div>
</div>
<div class="right">
<table cellspacing="2" align="center" width="100%" cellpadding="8px" border="1">
<tbody>
<tr>
<th>节次</th>
<th>星期一</th>
<th>星期二</th>
<th>星期三</th>
<th>星期四</th>
<th>星期五</th>
</tr>
<tr>
<td>1</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>2</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>3</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
name: 'Drag',
data() {
return {
itemList: [
{ color: 'red' },
{ color: 'pink' },
{ color: 'green' },
{ color: 'blue' },
{ color: 'purple' },
{ color: 'yellow' },
],
}
},
mounted() {
},
}
</script>
<style lang='scss' scoped>
.screen-view {
height: 100%;
padding: 10px;
box-sizing: border-box;
display: flex;
.left {
width: 200px;
height: 100%;
background: #ccc;
float: left;
margin-right: 10px;
}
.right {
width: calc(100% - 210px);
height: 100%;
padding: 10px;
background: #ccc;
box-sizing: border-box;
table th {
background: #eee;
width: 120px;
height: 60px;
text-align: center;
}
table td {
width: 120px;
height: 60px;
text-align: center;
}
table td:nth-child(1) {
background: #eee;
}
}
.item { // 颜色格子的样式要写在外面
width: 120px;
height: 50px;
margin: auto;
cursor: move;
}
.drop-over {
background-color: #393333;
}
}
</style>

2.给元素赋予可拖拽功能
要想实现把左边的颜色格子拖拽到右边的表格里面,就要先给需要拖拽的元素一个属性draggable
并且值设置为true
。
html
<div class="item" v-for="item in itemList" :key="item.color" :style="{ background: item.color }" draggable="true"></div>
此时这个元素就可以拖拽了,试一下。

3.拖拽相关事件
会发现元素已经是可以拖拽的了,但是为什么拖拽以后没有效果呢,那是需要我们自己去定义的,当一个元素可以拖拽并且被拖拽的时候,会触发一系列的事件,所以接下来我们就要监控这些被拖拽的元素,监控它们的拖拽事件,当然有一个更好的办法就是就是直接监控他们的父元素,使用事件委托。
我们使用事件委托找到它们的父元素,这个时候我们就不要找这个left
,而是找这个screen-view
,因为我们要拖拽到表格啊,所以表格那边也要触发拖拽事件,而表格和颜色格子的最近的共同父元素就是screen-view
。
找到父元素以后,有哪些事件可以监控呢?第一个就是dragstart
,这个事件就是拖拽开始的事件。
js
mounted() {
// 获取拖拽区域
const container = document.querySelector('.screen-view
// 监听拖拽开始事件 只在开始拖拽时触发一次
container.addEventListener('dragstart', (e) => {
console.log('dragstart', e.target);
});
},

可以看到每次拖拽都只会触发一次事件,然后我们也能根据target
去获取到被拖拽的元素。
然后我们就可以监控dragover
事件,这个事件表示拖拽这个元素拖拽到哪个地方上,这个事件会触发的比较频繁。
js
mounted() {
// 获取拖拽区域
const container = document.querySelector('.screen-view');
// 监听拖拽开始事件 只在开始拖拽时触发一次
container.addEventListener('dragstart', (e) => {
// console.log('dragstart', e.target);
});
// 监听拖拽到哪个元素上面的事件 不停触发
container.addEventListener('dragover', (e) => {
e.preventDefault(); // 阻止默认行为,允许拖拽
console.log('dragover', e.target);
});
},
bash

可以看到这个事件触发的很频繁,它有点像鼠标的move
事件,只要在元素上面就会触发,我们也可以根据target
去知道此时元素被拖拽到哪个元素上面。还有一个事件叫做dragenter
,这个事件是移动到某个元素的时候就会触发,这个事件有点像鼠标移入的事件mouseenter
,它只会触发一次,就是每次移动到一个新的元素上的时候就会触发,不会像dragover
事件不停的触发。
js
mounted() {
// 获取拖拽区域
const container = document.querySelector('.screen-view');
// 监听拖拽开始事件 只在开始拖拽时触发一次
container.addEventListener('dragstart', (e) => {
// console.log('dragstart', e.target);
});
// 监听拖拽到哪个元素上面的事件 不停触发
container.addEventListener('dragover', (e) => {
e.preventDefault(); // 阻止默认行为,允许拖拽
// console.log('dragover', e.target);
});
// 监听拖拽到哪个元素上面的事件 只触发一次
container.addEventListener('dragenter', (e) => {
console.log('dragent', e.target);
});
},

可以看到它只会在移动到一个新的元素上的时候才会触发,我们也可以根据target
去知道此时元素被拖拽到哪个元素上面。然后还有一个事件叫做drop
事件,这个事件会在拖拽元素放手时触发。
js
mounted() {
// 获取拖拽区域
const container = document.querySelector('.screen-view
// 监听拖拽开始事件 只在开始拖拽时触发一次
container.addEventListener('dragstart', (e) => {
// console.log('dragstart', e.target);
});
// 监听拖拽到哪个元素上面的事件 不停触发
container.addEventListener('dragover', (e) => {
e.preventDefault(); // 阻止默认行为,允许拖拽
// console.log('dragover', e.target);
});
// 监听拖拽到哪个元素上面的事件 只触发一次
container.addEventListener('dragenter', (e) => {
// console.log('dragent', e.target);
});
// 监听拖拽元素放手时触发的事件 只在放手时触发一次
container.addEventListener('drop', (e) => {
e.preventDefault(); // 阻止默认行为
console.log('drop', e.target);
});
},

可以看到这个事件只会在拖拽元素放手时触发,且也可以通过target
去知道在哪个元素上面放的手。至此拖拽相关的事件就完成了。我们总结一下有哪些事件。
事件名称 | 说明 |
---|---|
dragstart | 监听拖拽开始事件,只会在开始的时候触发 |
dragover | 监听拖拽到哪个元素上面的事件,会不停的触发,类似鼠标的move 事件 |
dragenter | 监听拖拽到哪个元素上面的事件,会不停的触发,类似鼠标的mouseenter 事件 |
drop | 监听拖拽元素放手时触发的事件,只在放手时触发一次 |
4.修改拖拽时的鼠标效果
事件已经说明白了,剩下的就是我们如何使用的问题了。
不知道有没有细心的人发现,我们在拖拽元素的时候鼠标变成了一个加号,这是因为默认以为你这个拖拽是要复制,所以给了一个加号,至于是不是复制,这就由我们的代码决定,它只是一个鼠标效果而已,而且我们也可以去修改这个效果,就在dragstart
事件上。
我们可以在拖拽开始的时候通过e.dataTransfer.effectAllowed
可以去设置拖拽的效果,值有move
,copy
等等,默认是copy
,那问题来了,它怎么知道我们是想要移动还是想要复制呢?这就很简单了,我们想要的效果是移动左侧的颜色格子的时候是复制效果,复制到右边的表格,此时就可以给左边的颜色格子一个自定义属性data-effect
赋值为copy
,此时就可以知道移动这个元素的时候是复制了。
html
<div class="item" v-for="item in itemList" :key="item.color" :style="{ background: item.color }" draggable="true" data-effect="copy"></div>
js
// 监听拖拽开始事件
container.addEventListener('dragstart', (e) => {
e.dataTransfer.effectAllowed = e.target.dataset.effect; // 设置拖拽效果
});
5.修改移动位置的背景色
现在我们想在拖拽元素经过别的元素的时候就修改这个元素的背景色,表示此时经过了这个元素,那这我们该怎么做呢?
这就可以用到dragenter
事件了,当我们移动到某元素的时候,我们就去改变它的背景色。
js
// 监听拖拽到哪个元素上面的事件 只触发一次
container.addEventListener('dragenter', (e) => {
e.target.classList.add('drop-over'); // 添加样式
});

会发现颜色都变了,这是因为dragenter
事件不仅会在子元素上触发,也会在父元素上触发,所以整个就变了,这肯定不是我们想要的,我们想改变的是那些单元格的背景色,所以此时我们就可以给单元格一个自定义属性data-drop
,设置为true
,然后我们拖拽颜色格子到某个元素的时候就可以判断是否有这个自定义属性且为true
就好了。其实还可以改进下,把单元格的自定义属性data-drop
值设置为copy
,这就表示只有copy
效果的拖拽元素经过单元格的时候才会变色,其他效果的不会变色。
html
<tr>
<td>1</td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
<tr>
<td>2</td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
<tr>
<td>3</td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
js
// 监听拖拽到哪个元素上面的事件 只触发一次
container.addEventListener('dragenter', (e) => {
if (e.target.dataset.drop === e.dataTransfer.effectAllowed) { // 注意此时这个 e 是能够在各个事件里面共享的
e.target.classList.add('drop-over'); // 添加样式
}
});

会发现还有一个问题,就是原本经过的单元格离开以后颜色没有变回去,所以我们还要移除前面单元格的背景色。
js
// 监听拖拽到哪个元素上面的事件 只触发一次
container.addEventListener('dragenter', (e) => {
//清除所有的 drop-over 样式
document.querySelectorAll('.drop-over').forEach(el => {
el.classList.remove('drop-over');
if (e.target.dataset.drop === e.dataTransfer.effectAllowed) { // 注意此时这个 e 是能够在各个事件里面共享的
e.target.classList.add('drop-over'); // 添加样式
}
});

这样效果就已经完成了。
6.把拖拽元素放置到目标元素上
我们前面说了这么多,都没有讲到怎么把要拖拽的元素放置到目标元素上,这点就要用到drop
事件了。
首先要判断这个目标元素是否是接受拖拽效果的,比如我们前面设置的单元格是接受copy
效果的,然后就是要获取到拖拽的元素,而在drop
事件中的e.target
已经获取不到拖拽的元素了,它只能获取到目标元素,所以我们要在dragstart
事件上就给拖拽元素赋值到一个变量上,再然后就是要清楚目标元素上的背景色,还有就是要区分这是copy
效果的还是move
效果的,最后才是复制一份拖拽元素然后给它加到目标元素上。
js
// 监听拖拽开始事件
container.addEventListener('dragstart', (e) => {
e.dataTransfer.effectAllowed = e.target.dataset.effect; // 设置拖拽效果
this.source = e.target; // 记录拖拽的元素
});
// 监听拖拽元素放手时触发的事件
container.addEventListener('drop', (e) => {
e.preventDefault(); // 阻止默认行为
if (e.target.dataset.drop === e.dataTransfer.effectAllowed) {
e.target.classList.remove('drop-over'); // 移除样式
if (e.dataTransfer.effectAllowed === 'copy') {
const newNode = this.source.cloneNode(true); // 克隆一个新的节点
e.target.appendChild(newNode); // 添加到目标元素中
}
}
});

这样拖拽效果就实现了。
7.优化添加背景色效果
其实上面还是有点瑕疵,就是会发现在第二次拖红色格子到已经有颜色格子的单元格的时候是没有变色的,只有移动下位置让红色格子接触到旁边空余的单元格的时候才会变色,这不是我们想要的,我们想要的是无论有没有颜色格子,移动过去就都要变色,因为我可能要替换这个颜色格子呢。
其实这也很简单,第二次移动获取没有变色无非就是因为判断移动经过的元素是已经放置好的颜色格子,而不是单元格,我们可以去获取放置好的颜色格子的父元素,而且这也是必须要做的,因为我们想要改变颜色的是容器,但是当容器里面已经有颜色格子的时候,我们再经过很可能经过的是这个颜色格子,也就是拖拽过后的元素,这就会影响我们改变容器的颜色,所以我们应该去获取经过元素的父元素,如果父元素也没有data-drop
属性那就再往上找,直到找到,如果到最后也没有才说明这不是容器。
所以我们前面那个修改移动位置的背景色的方法就得改变一下。
js
// 获取元素是否有自定义属性 data-drop
function getDropNode(node) {
// 递归获取有 data-drop 属性的节点
if (node.dataset && node.dataset.drop) {
// 如果有自定义属性 data-drop就直接return
return node;
} else if (node.parentNode) {
// 如果没有且还有父元素 继续递归
return getDropNode(node.parentNode);
} else {
// 如果没有父元素了 说明到顶了 还没有找到就 return null
return null;
}
}
// 监听拖拽到哪个元素上面的事件 只触发一次
container.addEventListener('dragenter', (e) => {
//清除所有的 drop-over 样式
document.querySelectorAll('.drop-over').forEach(el => {
el.classList.remove('drop-over');
});
const dropNode = getDropNode(e.target); // 获取有 data-drop 属性的节点
if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
dropNode.classList.add('drop-over'); // 添加样式
}
});
// 监听拖拽元素放手时触发的事件
container.addEventListener('drop', (e) => {
e.preventDefault(); // 阻止默认行为
const dropNode = getDropNode(e.target); // 获取有 data-drop 属性的节点
// 判断如果有 data-drop 属性 且 data-drop 的值等于拖拽效果
if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
// 移除样式
dropNode.classList.remove('drop-over');
// 根据拖拽效果 进行不同的操作
if (dropNode.dataset.drop === 'copy') {
const newNode = this.source.cloneNode(true); // 克隆一个新的节点
dropNode.appendChild(newNode); // 添加到目标元素中
}
}
});

会发现效果已经实现了,但是还是有一个问题,就是如果单元格已经有颜色格子了,再拖拽过去新的颜色格子,我们想要的效果是替换,而不是叠加,所以我们要把原先的颜色格子去掉。
js
// 监听拖拽元素放手时触发的事件
container.addEventListener('drop', (e) => {
e.preventDefault(); // 阻止默认行为
const dropNode = getDropNode(e.target); // 获取有 data-drop 属性的节点
// 判断如果有 data-drop 属性 且 data-drop 的值等于拖拽效果
if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
// 移除样式
dropNode.classList.remove('drop-over');
// 根据拖拽效果 进行不同的操作
if (dropNode.dataset.drop === 'copy') {
dropNode.innerHTML = ''; // 清空目标元素
const newNode = this.source.cloneNode(true); // 克隆一个新的节点
dropNode.appendChild(newNode); // 添加到目标元素中
}
}
});

8.把单元格的颜色格子拖出去到左侧
现在我们已经实现把左侧的颜色格子给复制到单元格中,那我们就要实现另外一个效果,就是把单元格的颜色格子移动到左侧,但是我们知道单元格里的颜色格子的data-effect
是copy
,所以我们要改成move
,然后把左侧的容器的data-drop
设置成move
就行了。然后在drop
事件中判断如果效果为move
,就把拖拽元素给去掉就好了。
js
// 监听拖拽元素放手时触发的事件
container.addEventListener('drop', (e) => {
e.preventDefault(); // 阻止默认行为
const dropNode = getDropNode(e.target); // 获取有 data-drop 属性的节点
// 判断如果有 data-drop 属性 且 data-drop 的值等于拖拽效果
if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
// 移除样式
dropNode.classList.remove('drop-over');
// 根据拖拽效果 进行不同的操作
if (dropNode.dataset.drop === 'copy') {
dropNode.innerHTML = ''; // 清空目标元素
const newNode = this.source.cloneNode(true); // 克隆一个新的节点
newNode.dataset.effect = 'move'; // 设置新的节点的拖拽效果为 move
dropNode.appendChild(newNode); // 添加到目标元素中
} else if (dropNode.dataset.drop === 'move') {
// move
this.source.remove(); // 移除拖拽的元素
}
}
});
html
<div class="left" data-drop="move">
<div class="item" v-for="item in itemList" :key="item.color" :style="{ background: item.color }"
draggable="true" data-effect="copy"></div>
</div>

93.全部代码
html
<template>
<div class="screen-view">
<div class="left" data-drop="move">
<div class="item" v-for="item in itemList" :key="item.color" :style="{ background: item.color }"
draggable="true" data-effect="copy"></div>
</div>
<div class="right">
<table cellspacing="2" align="center" width="100%" cellpadding="8px" border="1">
<tbody>
<tr>
<th>节次</th>
<th>星期一</th>
<th>星期二</th>
<th>星期三</th>
<th>星期四</th>
<th>星期五</th>
</tr>
<tr>
<td>1</td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
<tr>
<td>2</td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
<tr>
<td>3</td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
name: 'Drag',
data() {
return {
itemList: [
{ color: 'red' },
{ color: 'pink' },
{ color: 'green' },
{ color: 'blue' },
{ color: 'purple' },
{ color: 'yellow' },
],
source: null
}
},
mounted() {
// 获取拖拽区域
const container = document.querySelector('.screen-view');
// 监听拖拽开始事件
container.addEventListener('dragstart', (e) => {
e.dataTransfer.effectAllowed = e.target.dataset.effect; // 设置拖拽效果
this.source = e.target; // 记录拖拽的元素
});
// 监听拖拽到哪个元素上面的事件 不停触发 没用到
container.addEventListener('dragover', (e) => {
e.preventDefault(); // 阻止默认行为,允许拖拽
});
// 获取元素是否有自定义属性 data-drop
function getDropNode(node) {
// 递归获取有 data-drop 属性的节点
if (node.dataset && node.dataset.drop) {
// 如果有自定义属性 data-drop就直接return
return node;
} else if (node.parentNode) {
// 如果没有且还有父元素 继续递归
return getDropNode(node.parentNode);
} else {
// 如果没有父元素了 说明到顶了 还没有找到就 return null
return null;
}
}
// 监听拖拽到哪个元素上面的事件 只触发一次
container.addEventListener('dragenter', (e) => {
//清除所有的 drop-over 样式
document.querySelectorAll('.drop-over').forEach(el => {
el.classList.remove('drop-over');
});
const dropNode = getDropNode(e.target); // 获取有 data-drop 属性的节点
if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
dropNode.classList.add('drop-over'); // 添加样式
}
});
// 监听拖拽元素放手时触发的事件
container.addEventListener('drop', (e) => {
e.preventDefault(); // 阻止默认行为
const dropNode = getDropNode(e.target); // 获取有 data-drop 属性的节点
// 判断如果有 data-drop 属性 且 data-drop 的值等于拖拽效果
if (dropNode && dropNode.dataset.drop === e.dataTransfer.effectAllowed) {
// 移除样式
dropNode.classList.remove('drop-over');
// 根据拖拽效果 进行不同的操作
if (dropNode.dataset.drop === 'copy') {
dropNode.innerHTML = ''; // 清空目标元素
const newNode = this.source.cloneNode(true); // 克隆一个新的节点
newNode.dataset.effect = 'move'; // 设置新的节点的拖拽效果为 move
dropNode.appendChild(newNode); // 添加到目标元素中
} else if (dropNode.dataset.drop === 'move') {
// move
this.source.remove(); // 移除拖拽的元素
}
}
});
},
}
</script>
<style lang='scss' scoped>
.screen-view {
height: 100%;
padding: 10px;
box-sizing: border-box;
display: flex;
.left {
width: 200px;
height: 100%;
background: #ccc;
float: left;
margin-right: 10px;
}
.right {
width: calc(100% - 210px);
height: 100%;
padding: 10px;
background: #ccc;
box-sizing: border-box;
table th {
background: #eee;
width: 120px;
height: 60px;
text-align: center;
}
table td {
width: 120px;
height: 60px;
text-align: center;
}
table td:nth-child(1) {
background: #eee;
}
}
.item {
width: 120px;
height: 50px;
margin: auto;
cursor: move;
}
.drop-over {
background-color: #393333;
}
}
</style>