首先演示一下最終效果:html
流暢的拖動和交換位置效果,並實時更新數據 vue
支持組件的樣式和內容自定義 git
這是此次系列文章的第二篇,我本身封裝了一個用vue實現的拖拽排列卡片組件,而且發佈到npm,詳細地記錄下來了總體制做過程。總共有三篇文章,介紹組件的製做思路和遇到的問題,以及在發佈到npm上並下載使用的過程當中,發生了什麼問題並如何解決。github
先肯定初步要實現功能的大體需求:npm
建議看到一半不知道我在寫什麼的的小夥伴,直接去源碼倉庫看一下個人那個源碼。只想快速瞭解一下的就只看下面問題的總體思路就能夠了!api
首先咱們先要將卡片結構製做出來,讀取數據循環生成卡片。數組
<!-- 外層的div是用於制定卡片的範圍包括外面的margin -->
<div
class="cardBorderBox"
v-for="item of listData"
:key="item.id"
:id="item.id"
>
<!-- 裏面的div是用於顯示卡片自己的內容 -->
<div class="cardInsideBox" >
<div class="topWrapBox">
<!-- 這裏是標題欄,用於添加點擊事件 -->
</div>
<div class="emptyContent">
<!-- 這裏是內容部分 -->
</div>
</div>
</div>
<script>
export default {
//name記得必定要定義
name: "cardDragger",
data(){
return {
listData: [
{
positionNum: 1, // 位置號碼,卡片的位置根據這個計算生成
name: "演示卡片1", // 卡片標題
id: "card1", // 用於辨識的卡片ID
},
]
}
},
}
</script>
複製代碼
卡片還須要對位置和樣式進行調整,須要的其餘參數有:bash
data(){
return {
colNum:2, //一行有多少列
cardOutsideWidth:590, //單個卡片的外範圍寬度
cardOutsideHeight:380, //單個卡片的外範圍高度
cardInsideWidth:default:560, //單個卡片的內容寬度
cardInsideHeight:default:320, //單個卡片的內容高度
mousedownTimer: null //用於記錄卡片當前是否在過渡狀態中的定時器
}
}
複製代碼
卡片的佈局採用absolute定位,便於製做過渡動畫。width和height使用設定的卡片外圍寬高。dom
<template>
<div
class="cardBorderBox"
v-for="item of listData"
:key="item.id"
:id="item.id"
:style="{ width:cardOutsideWidth+'px', height:cardOutsideHeight+'px' }"
>
<!-- 省略部分代碼 -->
<div>
</template>
<script>
//總體就是按列數的限定,從左往右一行一行地排列數據
computeLeft(num) {
//left爲(位置號碼-1)%列數*卡片外圍寬度
return (num-1) % this.colNum * this.cardOutsideWidth;
},
computeTop(num) {
//top爲(位置號碼/列數)向上取整,減去1,再乘以卡片外圍高度
return (Math.ceil(num / this.colNum) - 1) * this.cardOutsideHeight;
}
</script>
<!-- 省略部分樣式代碼 -->
複製代碼
在首次加載和監聽到卡片數量產生變更時,須要從新根據自身的位置號碼計算生成卡片的top和left。保證異步數據的加載也能讀取到。異步
//判斷卡片的selectState是否存在,不存在則添加false
methods:{
addCardStyle(){
this.$nextTick(()=>{
this.listData.forEach(item=>{
document.querySelector('#'+item.id).style.top = this.computeTop(item.positionNum)+'px'
document.querySelector('#'+item.id).style.left = this.computeLeft(item.positionNum)+'px'
})
})
}
},
watch:{
listData:{
handler:function(){
this.addCardStyle()
},
immediate: true
}
}
複製代碼
接下來咱們須要在全部內容的最外面再包裹一層div,再添加上position:relative,根據listData的數量設定div的寬高。
<!--
首先,absolute是根據第一個父元素不爲static 定位的元素進行定位
其次,肯定寬高是由於將卡片移動的的時候,寬高會根據內容自適應,這裏不須要寬高自適應。
寬度爲:列數*卡片外圍寬度
高度爲:最後一個卡片的top+卡片外圍的高度
-->
<div
:style="{ position:'relative', height:computeTop(listData.length)+cardOutsideHeight+'px', width:cardOutsideWidth*colNum+'px'}"
>
<!-- computeTop()方法是上面計算卡片top的方法 -->
<!-- 卡片代碼 -->
</div>
複製代碼
而後咱們就給普通卡片的標題欄添加點擊事件,當鼠標點擊標題欄的時候,先判斷進行過渡動畫的定時器是否爲空,爲空的話直接返回。不爲空則執行點擊事件.
<div
class="cardBorderBox"
v-for="item of listData"
:key="item.id"
:id="item.id"
>
<div class="cardInsideBox" >
<div @mousedown="touchStart($event,item.id)" class="topWrapBox">
<!-- 標題欄添加點擊事件 -->
</div>
<div class="emptyContent">
<!-- 這裏是內容部分 -->
</div>
</div>
</div>
methods: {
//event爲鼠標的點擊事件,selectId是當前數據的id
touchStart(event, selectId) {
//其餘卡片正在動畫中的時候不能夠再次點擊,不然動畫和數據會出錯。
if (this.mousedownTimer) {
return false;
}
const that = this;
//選中的卡片的dom和數據
let selectDom = document.getElementById(selectId);
let selectMenuData = this.data.find(item => {
return item.id === selectId;
});
//獲取屏幕滾動條位置
let originTop = document.body.scrollTop === 0 ?
document.documentElement.scrollTop : document.body.scrollTop;
let scrolTop = originTop;
//記錄卡片的top和left
let moveTop
let moveLeft
//記錄起始選中位置
let OriginObjPosition = {
left: 0,
top: 0,
originNum: -1
};
//起始鼠標信息
let OriginMousePosition = {
x: 0,
y: 0
};
//記錄交換位置的號碼
let OldPositon = null;
let NewPositon = null;
//1.保存點擊的起始鼠標位置
OriginMousePosition.x = event.screenX;
OriginMousePosition.y = event.screenY;
//2.給選中卡片一個transition:none的class,去除默認過渡
selectDom.classList.add('d_moveBox')
//3.保存如今卡片的top和left
moveLeft = OriginObjPosition.left = parseInt(
//這裏獲取到的left是帶單位的字符串,要轉換成純數字
selectDom.style.left.slice(0, selectDom.style.left.length - 2)
);
moveTop = OriginObjPosition.top = parseInt(
selectDom.style.top.slice(0, selectDom.style.top.length - 2)
);
//4.添加其餘鼠標事件
document.addEventListener("mousemove", mouseMoveListener);
document.addEventListener("mouseup", mouseUpListener);
document.addEventListener("scroll", mouseScroll);
//省略部分代碼
}
}
複製代碼
鼠標移動、鬆開、滾輪事件也添加了。剩下的就是完善每一個事件的內容了。首先是鼠標移動事件,咱們須要監聽鼠標的當前位置和原先位置進行對比,再調整當前卡片的top和left,就可完成點擊卡片並移動卡片的效果。
methods: {
//全部其餘函數都添加在touchStart方法裏,共同使用點擊事件的數據
touchStart(event, selectId) {
//省略部分代碼
function mouseMoveListener(event) {
//在原來的top和left基礎上,加上鼠標的偏移量
moveTop = OriginObjPosition.top + ( event.screenY - OriginMousePosition.y );
moveLeft = OriginObjPosition.left + ( event.screenX - OriginMousePosition.x );
document.querySelector(".d_moveBox").style.left = moveLeft + "px";
document.querySelector(".d_moveBox").style.top = moveTop + (scrolTop - originTop) + "px"; //這裏要加上滾動的高度
}
}
}
複製代碼
鼠標滾輪事件也差很少,監聽滾動的具體,對卡片的位置進行改變。
function mouseScroll(event) {
scrolTop = document.body.scrollTop === 0
? document.documentElement.scrollTop
: document.body.scrollTop;
document.querySelector(".d_moveBox").style.top = moveTop + scrolTop - originTop + "px";
}
複製代碼
在上面的鼠標移動事件中,咱們調用檢測函數,檢測當前移動位置是否有卡片在下方,但須要對檢測函數進行節流,不然檢測頻率過高影響性能。卡片移動至另外一張卡片的某一方向距離超過百分之50的距離時,則進行位置交換。(這裏檢測的是以卡片外圍寬高進行計算的)
methods: {
touchStart(event, selectId) {
//用於保存檢測位置的定時器
let DectetTimer = null;
//省略部分代碼...
function mouseMoveListener(event) {
//省略部分代碼...
//在鼠標移動的監聽中添加以下代碼
if (!DectetTimer) {
DectetTimer = setTimeout(()=>{
//節流調用檢測函數,傳入當前位置信息
cardDetect(moveTop + (scrolTop - originTop),moveLeft)
//調用結束清空定時器
DectetTimer = null;
}, 200);
}
}
function cardDetect(moveItemTop, moveItemLeft){
//計算當前移動卡片位於卡片的哪一行哪一列
let newWidthNum = Math.round((moveItemLeft/ that.cardOutsideWidth))+1
let newHeightNum = Math.round((moveItemTop/ that.cardOutsideHeight))
//若是移動卡片至範圍外則不會有任何操做,直接返回
if(newHeightNum>(Math.ceil(that.listData.length / that.colNum) - 1)||
newHeightNum<0||
newWidthNum<=0||
newWidthNum>that.colNum){
return false
}
//將計算的行列轉換爲位置號碼
const newPositionNum = (newWidthNum) + newHeightNum * that.colNum
if(newPositionNum!==selectMenuData.positionNum){
//尋找當前位置號碼有沒有卡片數據
let newItem = that.listData.find(item=>{
return item.positionNum === newPositionNum
})
//有卡片數據的話就進行交換
if( newItem ){
swicthPosition(newItem, selectMenuData);
}
}
}
}
}
複製代碼
當檢測獲得的位置號碼,與現有的其餘普通卡片位置號碼重複時,則斷定爲須要交換位置。交換的狀況分爲位置號碼從小移動到大,和從大移動到小兩種狀況。
//省略部分代碼
function swicthPosition(newItem, originItem) {
OldPositon = originItem.positionNum;
NewPositon = newItem.positionNum;
that.$emit('swicthPosition',OldPositon,NewPositon,originItem)
//位置號碼從小移動到大
if (NewPositon > OldPositon) {
let changeArray = [];
//從小移動到大,那小的號碼就會空出來,其他卡片應往前移動一位
//找出兩個號碼中間對應的卡片數據
for (let i = OldPositon + 1; i <= NewPositon; i++) {
let pushData = that.data.find(item => {
return item.positionNum === i;
});
changeArray.push(pushData);
}
for (let item of changeArray) {
//vue的$set實時更改數據
that.$set(item, "positionNum", item.positionNum - 1);
//原生js調整卡片動畫
document.querySelector('#'+item.id).style.top = that.computeTop(item.positionNum)+'px'
document.querySelector('#'+item.id).style.left = that.computeLeft(item.positionNum)+'px'
}
//正在拖動的卡片就不須要動畫了
that.$set(originItem, "positionNum", NewPositon);
}
//位置號碼從大移動到小
if (NewPositon < OldPositon) {
let changeArray = [];
//從大移動到小,那大的號碼就會空出來,其他卡片應日後移動一位
//找出兩個號碼中間對應的卡片數據
for (let i = OldPositon - 1; i >= NewPositon; i--) {
let pushData = that.data.find(item => {
return item.positionNum === i;
});
changeArray.push(pushData);
}
for (let item of changeArray) {
that.$set(item, "positionNum", item.positionNum + 1);
document.querySelector('#'+item.id).style.top = that.computeTop(item.positionNum)+'px'
document.querySelector('#'+item.id).style.left = that.computeLeft(item.positionNum)+'px'
}
that.$set(originItem, "positionNum", NewPositon);
}
}
複製代碼
function mouseUpListener() {
/*首先清除位置檢測的定時器,
由於位置檢測的定時器,會在鼠標鬆開事件結束後執行,
會致使拖拽卡片都已經回到原位置並隱藏了,還會發生位置交換致使報錯。
應該調整爲,先清楚定時器,直接檢測,再添加卡片返回原處的動畫*/
clearTimeout(DectetTimer)
DectetTimer = null
//對鼠標鬆開位置直接進行最後一次位置檢測
cardDetect(moveTop + (scrolTop - originTop),moveLeft)
//設置卡片當前位置號碼計算生成的寬高,並添加transition進行過渡
document.querySelector(".d_moveBox").classList.add('d_transition');
document.querySelector(".d_moveBox").style.top = that.computeTop(selectMenuData.positionNum) + "px";
document.querySelector(".d_moveBox").style.left = that.computeLeft(selectMenuData.positionNum) + "px";
that.$emit('finishDrag',OldPositon,NewPositon,selectMenuData)
that.mousedownTimer = setTimeout(() => {
/*mousedownTimer是一個全局定時器,默認爲空。詳情可看倉庫源碼。
若鼠標鬆開,卡片過渡動畫開始時後則激活定時器,
時間到了的話就清空定時器內容。
保證在過渡動畫執行期間,不能點擊其餘卡片。
mousedownTimer在點擊事件開始時進行判斷,若不爲空則直接返回跳出點擊事件
*/
document.querySelector(".d_moveBox").classList.remove('d_transition')
document.querySelector(".d_moveBox").classList.remove('d_moveBox')
clearTimeout(that.mousedownTimer);
that.mousedownTimer = null;
}, 300);
//移除全部監聽
document.removeEventListener("mousemove", mouseMoveListener);
document.removeEventListener("mouseup", mouseUpListener);
document.removeEventListener("scroll", mouseScroll);
}
複製代碼
原來在data中的須要讓用戶自定義使用的屬性,都改成放在props中,並賦予默認值
//組件中:
props:{
data:{
type:Array,
//設定默認值,返回空數組
default: function () {
return []
}
},
colNum:{
type:Number,
default:2
},
cardOutsideWidth:{
type:Number,
default:590
},
cardOutsideHeight:{
type:Number,
default:380
},
cardInsideWidth:{
type:Number,
default:560
},
cardInsideHeight:{
type:Number,
default:320
}
},
//使用時:
<cardDragger
:data="componentData"
:colNum="3"
:cardOutsideWidth="360"
:cardInsideWidth="320"
:cardOutsideHeight="250"
:cardInsideHeight="210"
>
複製代碼
事件封裝也很簡單,只需在須要的地方調用自定義事件,例如,我在鼠標鬆開的事件中調用了:
//組件中$emit事件名+要傳遞的數據
function mouseUpListener() {
that.$emit('finishDrag',OldPositon,NewPositon,that.selectMenuData)
}
//使用時
<cardDragger
:data="componentData"
@finishDrag="finishDrag"
>
export default {
methods: {
finishDrag(OldPositon,NewPositon,originItem){
console.log(OldPositon,NewPositon,originItem)
}
}
}
複製代碼
插槽製做的話,先要肯定你有什麼內容是須要製做至插槽的。我這裏的話是要將標題欄的內容和卡片內容添加插槽,使用的是vue的具名插槽。把你原有的須要用插槽替換的內容放入slot裏面,當作默認內容就能夠了。
<div
class="d_cardBorderBox"
v-for="item of listData"
:key="item.id"
:id="item.id"
>
<div
class="d_cardInsideBox"
v-if="item.selectState===false"
>
<!--保留標題欄添加事件內容的div裏添加slot,保留點擊事件-->
<div @mousedown="touchStart($event,item.id)" class="d_topWrapBox">
<!--原來這裏應該是標題欄的內容,將slot添加至slot的默認值便可-->
<slot name="header" v-bind:item="item">
<div class="d_topMenuBox" >
<div class="d_menuTitle" >{{item.name}}</div>
</div>
</slot>
</div>
<slot name="content" v-bind:item="item" >
<div class="d_emptyContent">
卡片暫無內容
</div>
</slot>
</div>
</div>
複製代碼
還使用了做用域插槽,讓插槽內容可以訪問子組件中才有的數據。而且我還作了一些判斷,若data數據裏的componentData是存在的話就使用vue的component優先顯示。這裏就再也不贅述啦。
不採用h5的drag和drop是由於鼠標樣式會變成禁止符號和拖拽時會變成透明。不符合我對拖拽樣式的需求。
在顯示卡片和移動卡片的時候,是不能添加transition的,不然拖起來會有延遲。只有在鼠標鬆開後,使卡片返回原處的時候再添加transition進行過渡。又由於拖拽卡片是用v-if顯示的,在下次顯示拖拽卡片的時候transition已經被銷燬了。
添加了一個全局定時器,若鼠標鬆開,卡片過渡動畫開始時後則激活定時器,結束後清空定時器內容。點擊卡片的事件先判判定時器內容是否爲空再往下執行。
重寫了位置檢測,重寫了拖動,去除了好多無用的代碼。對異步數據沒法加載進行修復。目前以爲已經比一開始好了不少!請放心使用!
😃以上就是我製做這個組件的全過程啦,應該還有不少能夠優化的地方,歡迎你們指正。以爲有點意思的話記得點個贊呀~