以前在掘金上看到了一遍分享拖拽卡片組件的文章,看了大體思路,以爲很清晰,也想動手實現一下;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