以前在掘金上看到了一遍分享拖拽卡片组件的文章,看了大体思路,以为很清晰,也想动手实现一下;javascript
在过程当中发现了蛮多的细节问题,完成后对比了原做者的代码,发现许多能够优化的地方,在这里记录一下;css
如下是我的学习实现的demo和源码地址:html
在仓库中拿到dragCard.vue文件,引入到项目中,来看下面这个例子vue
// app.js
<template>
<div id="app">
<DragCard
:list="list"
:col="4"
:itemWidth="150"
:itemHeight="150"
@change="handleChange"
@mouseUp="handleMouseUp">
</DragCard>
</div>
</template>
<script>
import DragCard from './components/DragCard.vue'
export default {
name: 'app',
components: {
DragCard
},
data() {
return {
list: [
{head: '标题0', content: "演示卡片0"},
{head: '标题1', content: "演示卡片1"},
{head: '标题2', content: "演示卡片2"}
],
}
},
methods: {
handleChange(data) {
console.log(data);
},
handleMouseUp(data) {
console.log(data);
}
}
}
</script>
复制代码
经过组件属性和方法能够快速了解整个组件的使用;java
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
list | 卡片数据 | Array | [] |
col | 每一行显示多少张卡片 | Number | 3 |
itemWidth | 每一个卡片的宽度(包括外边距) | Number | 150 |
itemHeight | 每一个卡片的高度(包括外边距) | Number | 150 |
方法 | 说明 | 返回值 |
---|---|---|
@change | 当卡片位置变更的时候触发 | 返回的是数组中每一项的位置序号数组 |
@mouseUp | 当拖拽完卡片松手的时候触发 | 同上 |
::: tip 返回值是数组中每一项的位置序号集合;返回值数组index
和list
中的index
一致;后续咱们能够经过操做这两个数组,合并成[{ id: 'cardid1', seatid: '1' }...]
这样的形式传递给后端,修改卡片的位置数据;固然建议是在mouseUp的时候去发送请求更优; :::git
slotName | 说明 | data |
---|---|---|
head | 卡片的头部标题部分 | listItem |
content | 卡片内容部分 | listItem |
::: tip 这两个做用域插槽都有默认值,若是不填写的话,标题将显示list
中的head
属性,而内容将显示content
属性;两个slot
都带上了当前卡片的list
项数据;能够更加灵活的自定义卡片内容; :::github
absolute
布局,经过设置left
和top
,让卡片按顺序排列,所以传入的list
必须是正序的;props
传入的值,咱们能够计算出行列数,卡片位置等信息;mousemove
和mouseup
事件;这时候鼠标的移动距离就是卡片的移动距离;change
方法;mouseUp
方法;<div class="dragCard">
<div
class="dragCard_warpper"
ref="dragCard_warpper"
:style="dragCardWarpperStyle">
<div
v-for="(item, index) in list"
:key="index"
class="dragCard_item"
:style="initItemStyle(index)"
:ref="item.dragCard_id">
<div class="dragCard_content">
<div
class="dragCard_head"
@mousedown="touchStart($event, item)">
<slot name="head" :item="item" >
<div class="dragCard_head-defaut">
{{ item.head ? item.head : `卡片标题${index + 1}` }}
</div>
</slot>
</div>
<div class="dragCard_body">
<slot name="content" :item="item">
<div class="dragCard_body-defaut">
{{ item.content ? item.content : `暂无数据` }}
</div>
</slot>
</div>
</div>
</div>
</div>
</div>
复制代码
@mousedown
设置在dragCard_head
中,为了实现这一点,把slot
分为了两个部分,一个是head
标题部分,默认显示item.content
;一个是content
内容部分,默认显示item.head
,;用户能够经过slot
自定义卡片;slot知识点// app.js 使用自定义卡片样式
<template>
<div id="app">
<DragCard
:list="list"
:col="4"
:itemWidth="150"
:itemHeight="150"
@change="handleChange"
@mouseUp="handleMouseUp">
<template v-slot:head="{ item }">
<div class="dragHead">{{item.head}}</div>
</template>
<template v-slot:content="{ item }">
<div class="dragContent">{{item.content}}</div>
</template>
</DragCard>
</div>
</template>
复制代码
// ...
created() {
this.init();
},
methods: {
init () {
// 根据数组的长度length和每行个数col,能够计算出须要多少行row,超出不满一行算一行,用ceil向上取整;
this.row = Math.ceil(this.list.length / this.col);
// 计算出容器的宽高
this.dragCardWarpperStyle = `width: ${this.col * this.itemWidth}px; height:${this.row * this.itemHeight}px`;
/* * 这里处理下数组,引入两个重要的属性: * dragCard_id: * 给每个卡片建立一个惟一id,做为ref值,后续经过this.$refs[dragCard_id]获取卡片的dom * dragCard_index: * 这是每一个卡片的位置序号,用于记录卡片当前位置 * */
this.list.forEach((item, index) => {
this.$set(item, 'dragCard_index', index);
this.$set(item, 'dragCard_id', 'dragCard_id' + index);
});
},
// 经过index计算出每一个卡片的left和right
initItemStyle(INDEX) {
return {
width: this.itemWidth + 'px',
height: this.itemHeight + 'px',
left: (INDEX < this.col ? INDEX : (INDEX % this.col)) * this.itemWidth + 'px',
top: Math.floor(INDEX / this.col) * this.itemHeight + 'px'
};
}
}
复制代码
list
确定会有改变的场景,这时候咱们就要从新计算行列数,从新计算容器宽高等,其实也就是从新执行init
函数;因此咱们须要监听list
;watch: {
list: {
handler: function(newVal, oldVal) {
this.init();
},
immediate: true // 定义的时候就执行一次,因此created的时候就不须要执行init了
}
},
复制代码
在handleMousedown()
的时候直接定义handleMousemove()
和handleMouseUp()
事件,而且在handleMouseUp()
中移除;npm
首先是几个比较重要的变量和方法后端
itemList
:list
的拷贝,并加上后续须要用到的属性dom
(当前卡片的节点信息,经过ref获取), isMoveing
(标记当前卡片是否在移动中), left
, top
,数组
curItem
:当前卡片用的比较多,因此这里单独拿了出来,而且在移动的时候,单前卡片的过渡效果应该去除,否则移动会卡顿,而且z-index
应该在较高的层级
targetItem
: 即将交换位置的卡片对象,起始为null
mousePosition
:鼠标起始位置,移动后的鼠标位置减去起始位置,就是卡片的移动偏移量;
handleMousemove()
:鼠标移动
cardDetect()
:卡片移动检测,是否须要执行位置交换
swicthPosition()
:交换卡片位置
handleMouseUp()
:鼠标抬起
handleMousedown(e, optionItem) {
e.preventDefault();
let that = this;
if (this.timer) return false; // timer为全局的定时器,表示当前有卡片正在移动,直接返回;
// 拷贝一份list,并加上后续要使用的属性;
let itemList = that.list.map(item => {
// 若是ref是动态赋的值,存入$refs中会是一个数组;
let dom = this.$refs[item.dragCard_id][0];
let left = parseInt(dom.style.left.slice(0, dom.style.left.length - 2));
let top = parseInt(dom.style.top.slice(0, dom.style.top.length - 2));
let isMoveing = false; // 标记正在移动的卡片,正在移动的卡片不参与碰撞检测
return {...item, dom, left, top, isMoveing};
});
// 当前卡片对象用的比较多,用一个别名curItem把他存起来;
let curItem = itemList.find(item => item.dragCard_id === optionItem.dragCard_id);
curItem.dom.style.transition = 'none';
curItem.dom.style.zIndex = '100';
curItem.dom.childNodes[0].style.boxShadow = '0 0 5px rgba(0, 0, 0, 0.1)';
curItem.startLeft = curItem.left; // 起始的left
curItem.startTop = curItem.top; // 起始的top
curItem.OffsetLeft = 0; // left的偏移量
curItem.OffsetTop = 0; // top的偏移量
// 即将交换位置的对象
let targetItem = null;
// 记录鼠标起始位置
let mousePosition = {
startX: e.screenX,
startY: e.screenY
};
document.addEventListener("mousemove", handleMousemove);
document.addEventListener("mouseup", handleMouseUp);
// 鼠标移动
function handleMousemove(e) {}
// 卡片交换检测
function cardDetect() {}
// 卡片交换
function swicthPosition() {}
// 鼠标抬起
function handleMouseUp() {}
}
复制代码
鼠标当前的坐标减去起始的坐标,就是当前卡片的偏移量;
移动过程当中就能够执行卡片交换检测,为了提升性能,作了如下节流;200ms执行一次;
// 鼠标移动
function handleMousemove(e) {
curItem.OffsetLeft = parseInt(e.screenX - mousePosition.startX);
curItem.OffsetTop = parseInt(e.screenY - mousePosition.startY);
// 改变当前卡片对应的style
curItem.dom.style.left = curItem.startLeft + curItem.OffsetLeft + 'px';
curItem.dom.style.top = curItem.startTop + curItem.OffsetTop + 'px';
// 卡片交换检测,作一下节流
if (!DectetTimer) {
DectetTimer = setTimeout(() => {
cardDetect();
clearTimeout(DectetTimer);
DectetTimer = null;
}, 200)
}
}
复制代码
一开始想到的是用碰撞检测去作,循环整个itemList,而后对比当前卡片和每一项的距离;当小于设定的gap
的时候,就执行swicthPosition()
;
后面看了裂泉
的原文章后,发现以前的作法性能差太多了;一直在循环数组;
经过当前的位置和偏移量,能够计算出目标位置targetItemDragCardIndex
,判断一些临界值以后便执行交换函数;
// 卡片移动检测
function cardDetect() {
// 根据移动的距离计算出移动到哪个位置
let colNum = Math.round((curItem.OffsetLeft / that.itemWidth));
let rowNum = Math.round((curItem.OffsetTop / that.itemHeight));
// 这里的dragCard_index须要用到最初点击卡片的位置,由于curItem在后续的卡片交换中dragCard_index已经改变;
let targetItemDragCardIndex = optionItem.dragCard_index + colNum + (rowNum * that.col);
// 超出行列,目标位置不变或不存在都直接return;
if(Math.abs(colNum) >= that.col
|| Math.abs(rowNum) >= that.row
|| Math.abs(colNum) >= that.col
|| Math.abs(rowNum) >= that.row
|| targetItemDragCardIndex === curItem.dragCard_index
|| targetItemDragCardIndex < 0
|| targetItemDragCardIndex > that.list.length - 1) return false;
let item = itemList.find(item => item.dragCard_index === targetItemDragCardIndex);
item.isMoveing = true;
// 将目标卡片拷贝一份,主要是为了松开鼠标的时候赋值给当前卡片;
targetItem = {...item};
swicthPosition();
}
复制代码
卡片交换分为两种状况;
::: tip 注意
isMoveing = true
,并设置定时器300ms后清除isMoveing
itemList
中的属性,不须要改变list
中,等到最后松开鼠标的时候才同步到list
中 :::function swicthPosition() {
const dragCardIndexList = itemList.map(item => item.dragCard_index);
// 目标卡片位置大于当前卡片位置;
if (targetItem.dragCard_index > curItem.dragCard_index) {
for (let i = targetItem.dragCard_index; i >= curItem.dragCard_index + 1; i--) {
let item = itemList[dragCardIndexList.indexOf(i)];
let preItem = itemList[dragCardIndexList.indexOf(i - 1)];
item.isMoveing = true;
item.left = preItem.left;
item.top = preItem.top;
item.dom.style.left = item.left + 'px';
item.dom.style.top = item.top + 'px';
item.dragCard_index = that.list[dragCardIndexList.indexOf(i)].dragCard_index -= 1;
setTimeout(() => {
item.isMoveing = false;
}, 300)
}
}
// 目标卡片位置小于当前卡片位置;
if (targetItem.dragCard_index < curItem.dragCard_index) {
for (let i = targetItem.dragCard_index; i <= curItem.dragCard_index - 1; i++) {
let item = itemList[dragCardIndexList.indexOf(i)];
let nextItem = itemList[dragCardIndexList.indexOf(i + 1)];
item.isMoveing = true;
item.left = nextItem.left;
item.top = nextItem.top;
item.dom.style.left = item.left + 'px';
item.dom.style.top = item.top + 'px';
item.dragCard_index = that.list[dragCardIndexList.indexOf(i)].dragCard_index += 1;
setTimeout(() => {
item.isMoveing = false;
}, 300)
}
}
curItem.left = targetItem.left;
curItem.top = targetItem.top;
curItem.dragCard_index = targetItem.dragCard_index;
// 派发change事件通知父组件
that.$emit('change', itemList.map(item => item.dragCard_index));
}
复制代码
transition
在css
中设置了,这里把style
清除便可function handleMouseUp() {
//移除全部监听
document.removeEventListener("mousemove", handleMousemove);
document.removeEventListener("mouseup", handleMouseUp);
// 清除检测的定时器并作最后一次碰撞检测
clearTimeout(DectetTimer);
DectetTimer = null;
cardDetect();
// 把过渡效果加回去
curItem.dom.style.transition = '';
// 同步dragCard_index到list中;
that.list.find(item => item.dragCard_id === optionItem.dragCard_id).dragCard_index = curItem.dragCard_index;
curItem.dom.style.left = curItem.left + 'px';
curItem.dom.style.top = curItem.top + 'px';
// 派发mouseUp事件通知父组件
that.$emit('mouseUp', that.list.map(item => item.dragCard_index));
that.timer = setTimeout(() => {
curItem.dom.style.zIndex = '';
curItem.dom.childNodes[0].style.boxShadow = 'none';
clearTimeout(that.timer);
that.timer = null;
}, 300);
}
复制代码
到这里这个组件就完成啦!
最后贴上来自裂泉
的原文章连接: 跟我一块儿,从0实现并封装拖拽排列组件 ;这仍是一个系列文章,todo
中后续还会分享如何把组件上传到npm
;
dranein@163.com