Let’s first demonstrate the final result:
Smoothly drag and swap location effects, and update data in real time
Support component style and content customization
In the second article in this series, I packaged a drag and drop card component in VUE and posted it to NPM, documenting the entire process in detail. In total, there are three articles that describe how the component was made and the problems encountered, as well as what happened and how to solve the problems when it was published to NPM and downloaded for use.
- The first part is the usage document and introduction after component encapsulation
- The second part is the implementation of components and details
- Chapter 3: How to load on demand and download for use after packaging and uploading components to NPM
First determine the general requirements for the initial functionality to be implemented:
- Mouse click card can be moved, the mouse scroll also according to follow the scroll
- When you move a card to an area near the top of another card, you swap positions. When switching positions, the card in between automatically moves forward/back
- Release the mouse and return the card to original position/new position
- Expose properties, events to parent component calls, and make slots
I suggest that half of the friends who don’t know what I am writing, go directly to the source repository to have a look at my source. It is ok to see the integral train of thought of the following problem only to know only quickly!
Q1: How to implement card movement?
Overall idea:
- The layout of all cards is absolute. Top and left are calculated and displayed according to the position number and column number.
- When you click a card, check whether the card is dragging. If not, go to the next step, get the data and remove the card default transition.
- Click, global monitoring of mouse movement events, the number of mouse movement distance, the card will move the number of distance. You also need to listen for the window’s scroll event to do the same.
- When the mouse is released, clear all listening and restore the card to the position calculated according to the position number. Change the click state to false
Concrete implementation:
First of all, we need to make the card structure, read the data cycle to generate the card.
<! -- The outer div is used to define the scope of the card including the outer margin --> <div class="cardBorderBox"
v-for="item of listData"
:key="item.id"
:id="item.id"> <! Div is used to display the card itself --> <div class="cardInsideBox" >
<div class="topWrapBox"> <! </div> <div class="emptyContent"> <! </div> </div> </div>exportDefault {//name remember to define name:"cardDragger".data() {return{listData: [{positionNum: 1, // Position number, position of card based on this calculation generate name:"Presentation Card 1", // Card title id:"card1", // Card ID},]}},} </script>Copy the code
The card also needs to be adjusted for position and style. Other parameters required are:
data() {return{colNum:2, // How many columns are in a row / / a single card outside the range of highly cardInsideWidth: default: 560, / / the contents of a single card width cardInsideHeight: default: 320, / / the contents of a single card highly mousedownTimer: Null // Timer used to record whether the card is currently in transition state}}Copy the code
The layout of the card uses absolute positioning to facilitate the production of transition animation. Width and height use the specified card outer width and height.
<template>
<div
class="cardBorderBox"
v-for="item of listData"
:key="item.id"
:id="item.id"
:style="{ width:cardOutsideWidth+'px', height:cardOutsideHeight+'px' }"> <! ComputeLeft (num) {//left = (position number -1)% number of columns * card periphery widthreturn(num-1) % this.colNum * this.cardOutsideWidth; }, computeTop(num) {//top = (position number/column number), subtract 1, and multiply the card periphery heightreturn(Math.ceil(num / this.colNum) - 1) * this.cardOutsideHeight; } </script> <! -- Omit some style code -->Copy the code
When the number of cards changes during the first loading and monitoring, you need to calculate the top and left of the generated cards again based on the location number of the card. Ensure that asynchronous data can be read by loading.
// Check whether the card's selectState exists, or add it if it does notfalse
methods:{
addCardStyle(){
this.$nextTick(()=>{
this.listData.forEach(item=>{
document.querySelector(The '#'+item.id).style.top = this.computeTop(item.positionNum)+'px'
document.querySelector(The '#'+item.id).style.left = this.computeLeft(item.positionNum)+'px'
})
})
}
},
watch:{
listData:{
handler:function(){
this.addCardStyle()
},
immediate: true}}Copy the code
Next we need to wrap a div layer on the top of all the content, add position:relative, and set the width and height of the div according to the number of listData.
<! Absolute: First, absolute is based on the element whose first parent is not static. Second, width and height are determined because when the card is moved, the width and height adapt to the content. No width and height adaptation is required. <div :style= "color: RGB (51, 51, 51); font-size: 14px! Important; white-space: inherit! Important;"{ position:'relative', height:computeTop(listData.length)+cardOutsideHeight+'px', width:cardOutsideWidth*colNum+'px'}"> <! -- computeTop() is the method above that computes the top of cards --> <! </div>Copy the code
Then we will add click events to the title bar of ordinary cards. When the mouse clicks on the title bar, it will judge whether the timer for transition animation is empty or not. If so, it will return directly. If not empty, the click event is executed.
<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> TouchStart (event, selectId) {touchStart(event, selectId) {touchStart(event, selectId) {touchStart(event, selectId) {touchStart(event, selectId) {if (this.mousedownTimer) {
return false; } const that = this; // Dom and data for the selected cardlet selectDom = document.getElementById(selectId);
let selectMenuData = this.data.find(item => {
returnitem.id === selectId; }); // Get the screen scrollbar positionlet originTop = document.body.scrollTop === 0 ?
document.documentElement.scrollTop : document.body.scrollTop;
letscrolTop = originTop; // Record the top and left of the cardlet moveTop
letMoveLeft // Records the initial selection positionletOriginObjPosition = { left: 0, top: 0, originNum: -1 }; // Start mouse informationletOriginMousePosition = { x: 0, y: 0 }; // Record the switching location numberlet OldPositon = null;
letNewPositon = null; Originmouseposition. x = event.screenx; originMousePosition. x = event.screenx; OriginMousePosition.y = event.screenY; //2. Give the selected card a transition: None class, removing the default transition selectdom.classlist.add ('d_moveBox'Top and left moveLeft = originobjPosition. left = parseInt Want to convert into a pure digital 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. Add other mouse events document. The addEventListener ("mousemove", mouseMoveListener);
document.addEventListener("mouseup", mouseUpListener);
document.addEventListener("scroll", mouseScroll); // omit some code}}Copy the code
Mouse movement, release, and scroll events have also been added. All that remains is to refine the content of each event. First is the mouse movement event, we need to monitor the current position of the mouse and compare with the original position, and then adjust the top and left of the current card, to complete the effect of clicking the card and moving the card.
TouchStart (event, selectId) {// Omit some of the codefunctionMouseMoveListener (event) {// On top and 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"; }}}Copy the code
The same goes for the mouse wheel event, which listens for the scroll and changes the position of the card.
function mouseScroll(event) {
scrolTop = document.body.scrollTop === 0
? document.documentElement.scrollTop
: document.body.scrollTop;
document.querySelector(".d_moveBox").style.top = moveTop + scrolTop - originTop + "px";
}
Copy the code
Q2: How do you detect and exchange cards?
Overall idea:
- When a card is moved, the function that calculates the current position of the card belongs to the position number is called. If the position of the card is the same as that of the existing number and not its own number, the position is changed.
- During the exchange, compare whether the location number is changed from small to large or from large to small, and move the number in the middle one digit forward or one digit backward.
Concrete implementation:
In the mouse movement event above, we call the detection function to detect whether there is a card below the current moving position, but the detection function needs to be throttled, otherwise the detection frequency is too high and the performance will be affected. When a card moves more than 50 percent of the way to another card in a certain direction, a position swap occurs. (The measurement here is calculated based on the external width and height of the card)
Methods: {touchStart(event, selectId) {// Timer used to save the detection positionletDectetTimer = null; // omit some code...functionMouseMoveListener (event) {// Omit some code... // Add the following code to the mouse movement monitorif(! DectetTimer) { DectetTimer =setCardDetect (moveTop + (scrolTop-originTOP),moveLeft) // Call end empty timer DectetTimer = null; }, 200); }}functionCardDetect (moveItemTop, moveItemLeft){// Calculates which row and which column of the card is currently movinglet newWidthNum = Math.round((moveItemLeft/ that.cardOutsideWidth))+1
letNewHeightNum = math.round ((moveItemTop/ that.cardOutsideHeight)if(newHeightNum>(Math.ceil(that.listData.length / that.colNum) - 1)||
newHeightNum<0||
newWidthNum<=0||
newWidthNum>that.colNum){
return falseConst newPositionNum = (newWidthNum) + newHeightNum * that.colnumif(newPositionNum! . = = selectMenuData positionNum) {/ / have to find the current location number card datalet newItem = that.listData.find(item=>{
returnItem.positionnum === newPositionNum}if( newItem ){
swicthPosition(newItem, selectMenuData);
}
}
}
}
}
Copy the code
When the detected location number is the same as the existing location number of other common cards, it is determined that the location needs to be changed. The exchange is divided into two cases: the position number is moved from small to large, and from large to small.
// Omit some codefunction swicthPosition(newItem, originItem) {
OldPositon = originItem.positionNum;
NewPositon = newItem.positionNum;
that.$emit('swicthPosition', OldPositon NewPositon originItem) / / position number, moved to the big since childhoodif (NewPositon > OldPositon) {
letchangeArray = []; // If you move from small to large, the small number will be empty, and the rest of the cards should move forward one bit // Find the corresponding card data between the two numbersfor (let i = OldPositon + 1; i <= NewPositon; i++) {
let pushData = that.data.find(item => {
return item.positionNum === i;
});
changeArray.push(pushData);
}
for (letItem of changeArray) {//vue$setChange data that in real time.$set(item, "positionNum", item.positionNum - 1); // native JS adjusts the card animation document.querySelector(The '#'+item.id).style.top = that.computeTop(item.positionNum)+'px'
document.querySelector(The '#'+item.id).style.left = that.computeLeft(item.positionNum)+'px'} // The dragging card does not need to be animated that.$set(originItem, "positionNum", NewPositon); } // The location number is moved from large to smallif (NewPositon < OldPositon) {
letchangeArray = []; // If you move from big to small, the big number will be empty, and the rest of the cards should be moved back one bitfor (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(The '#'+item.id).style.top = that.computeTop(item.positionNum)+'px'
document.querySelector(The '#'+item.id).style.left = that.computeLeft(item.positionNum)+'px'
}
that.$set(originItem, "positionNum", NewPositon); }}Copy the code
Q3: Return to original position after mouse release?
Overall idea:
- When the mouse is released, empty the timer in position detection first, and then conduct the last position detection.
- Restore the card to the position corresponding to the position number, and add the timer of the same time as the card transition, clear the timer in the timer and remove the other class of the card. The next click can be performed only when the timer is empty.
Concrete implementation:
function mouseUpListener() {/* First clear the timer of position detection, because the timer of position detection will be executed after the end of the mouse release event, which will result in the drag cards have been back to the original position and hidden, and will also occur position swap resulting in an error. Should be adjusted to, first clear timer, direct detection, */ clearTimeout(DectetTimer) DectetTimer = NULL // Perform the last position detection directly on the mouse release position cardDetect(moveTop + (scrolTop -) OriginTop),moveLeft) // Set the current position of the card to calculate the width and height of the generated, and add 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 is a global timer and is null by default. Details can see the warehouse source code. If the mouse release, the card transition animation after the start of the activation timer, time to empty the timer content. Ensure that no other cards can be clicked during the transition animation. MousedownTimer determines when the click event starts. If it is not empty, it returns the click event */ document.querySelector(".d_moveBox").classList.remove('d_transition')
document.querySelector(".d_moveBox").classList.remove('d_moveBox') clearTimeout(that.mousedownTimer); that.mousedownTimer = null; }, 300); / / remove all listening document. RemoveEventListener ("mousemove", mouseMoveListener);
document.removeEventListener("mouseup", mouseUpListener);
document.removeEventListener("scroll", mouseScroll);
}
Copy the code
Q4: How to make component slot and attribute, event customization?
Overall idea:
- Properties: Define the data for data in props and set the default values
- Event: Just call $emit in some function in the component and listen while it is used
- Slot: Made using vUE named slot in update 2.6.0
Concrete implementation:
The properties in data that need to be used by users should be placed in props and given default values
// Props :{data:{type:Array, // Set the default value, return an empty 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"
>
Copy the code
Event encapsulation is also simple, just calling custom events where needed. For example, I called in the mouse release event:
/ / components$emitEvent name + data to be passedfunction mouseUpListener() {
that.$emit('finishDrag', OldPositon NewPositon that. SelectMenuData)} / / when using the < cardDragger: data ="componentData"
@finishDrag="finishDrag"
>
export default {
methods: {
finishDrag(OldPositon,NewPositon,originItem){
console.log(OldPositon,NewPositon,originItem)
}
}
}
Copy the code
Slot production, first to determine what you need to produce content into the slot. I’m adding the title bar content and card content to the slot, using the named slot of the VUE. Slot your existing content into the slot as the default content.
<div
class="d_cardBorderBox"
v-for="item of listData"
:key="item.id"
:id="item.id"
>
<div
class="d_cardInsideBox"
v-if="item.selectState===false"> <! -- Keep the title bar to add slot to the event content of the div, keep the click event --> <div @mousedown="touchStart($event,item.id)" class="d_topWrapBox"> <! <slot name= --> <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>Copy the code
Scoped slots are also used to give slot contents access to data only available in child components. In addition, I made some judgments. If componentData in data data exists, Vue component will be used to display it first. I won’t repeat it here.
Q5: What are the problems in production?
1. Why not use Drag and drop?
The H5 drag and drop are not used because mouse styles become forbidden symbols and become transparent when dragged. Doesn’t fit my needs for drag and drop styles.
2. When is the transition added to the drag card?
You can’t add transition to displaying or moving a card because it would delay dragging. Add transition only when you release the mouse and return the card to its original position. And because the dropcard is displayed with v-if, the next time the dropcard is displayed, the transition will be destroyed.
3. What if a quick click on another card before the animation ends gives an error?
Added a global timer, if the mouse release, the card transition animation after the start of the timer, timer content empty. Click the event of the card to determine whether the timer content is empty and then proceed.
4. What optimizations have you made since your first article?
Rewrote location detection, rewrote drag, got rid of a lot of useless code. Fix for asynchronous data not loading. At present, I feel much better than at the beginning! Please feel free to use!
😃 above is the whole process of making this component, there should be a lot of places can be optimized, welcome to correct. If you find something interesting, please give it a thumbs up