preface
I read the article about the drag-and-drop card component on The Nuggets before. After reading the general idea, I felt it was very clear and wanted to implement it.
In the process, I found a lot of details. After completion, I compared the original author’s code and found many places that can be optimized, which is recorded here.
The following is a personal learning implementation of demo and source address:
- Links to online
- The source address
use
Take the dragcard. vue file from the repository and import it into your project. Look at this example
// 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' title, content: "0" demo card}, {head: '1' title, content: "demo card 1"}, {the head: '2' title, content: "demo card 2"}],}}, the methods: { handleChange(data) { console.log(data); }, handleMouseUp(data) { console.log(data); } } } </script>Copy the code
Let’s look at the props and methods
Component properties and methods can be used to quickly understand the use of the entire component.
attribute
attribute | instructions | type | The default value |
---|---|---|---|
list | Card data | Array | [] |
col | How many cards are displayed per line | Number | 3 |
itemWidth | Width of each card (including margins) | Number | 150 |
itemHeight | Height of each card (including margins) | Number | 150 |
methods
methods | instructions | The return value |
---|---|---|
@change | Triggered when the card position changes | Returns an array of positional ordinals for each item in the array |
@mouseUp | Triggered when the card is released after being dragged | Same as above |
::: tip returns a set of positional ordinals for each item in the array; The array index returns the same value as the list index; [{id: ‘cardid1’, seatid: ‘1’}…] This form is passed to the back end to modify the card position data; Of course, it is better to send requests in mouseUp; : : :
Slot slot.
slotName | instructions | data |
---|---|---|
head | The header section of the card | listItem |
content | Card content | listItem |
::: tip Both scope slots have default values. If left blank, the header will display the head attribute in the list and the content attribute will display the content attribute. Both slots carry the list item data of the current card; More flexibility to customize card content; : : :
The specific implementation
Thinking about
- Page card using
absolute
Layout by settingleft
andtop
, let the cards be in order, so incominglist
It must be in positive order; - Initialize the style, pass
props
Incoming values, we can calculate the number of lines, card positions and other information; - Add a location attribute to each item in the array. Subsequent position swaps can be expanded by this location attribute, which is also the return value passed by subsequent triggering methods to the parent.
- When the mouse is pressed, the current position of the mouse is recorded as the starting position, and the current card is passed in as a parameter and bound
mousemove
andmouseup
Events; At this time the mouse movement distance is the card movement distance; - As the card moves, we calculate whether it is currently moving to another card position. If so, all the cards in between move back or forward, triggering the parent component’s
change
Methods; - When the mouse is released, the card returns to the target position, triggering the parent component’s
mouseUp
Methods;
First take a look at the page structure
<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 : 'Card title ${index + 1}'}} </div> </slot> <div class="dragCard_body"> <slot name="content" :item="item"> <div class="dragCard_body-defaut"> {{ item.content ? Item. The content: ` temporarily countless according to `}} < / div > < / slot > < / div > < / div > < / div > < / div > < / div >Copy the code
- Mouse click title can drag card, so
@mousedown
Set in thedragCard_head
In order to achieve this, theslot
It’s divided into two parts. One ishead
The title section, displayed by defaultitem.content
; One is thecontent
The content section is displayed by defaultitem.head
,; Users can useslot
Custom cards;Slot knowledge
// app.js uses custom card styles <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>Copy the code
- DragCardWarpperStyle is the style of the container. The width and height of the container are calculated by passing the props value. It should be calculated when the component is initialized, and included in init();
// ...
created() {
this.init();
},
methods: {
init () {
// Calculate the number of rows required based on the length of the array and the number of rows.
this.row = Math.ceil(this.list.length / this.col);
// Calculate the width and height of the container
this.dragCardWarpperStyle = `width: The ${this.col * this.itemWidth}px; height:The ${this.row * this.itemHeight}px`;
/* * this.$refs[dragCard_id] gets the dom of the card * dragCard_index: * This is the position number of each card, used to record the current position of the card * */
this.list.forEach((item, index) = > {
this.$set(item, 'dragCard_index', index);
this.$set(item, 'dragCard_id'.'dragCard_id' + index);
});
},
// Use index to calculate the left and right of each card
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'}; }}Copy the code
- Of course our card data comes in from the parent, so
list
There will certainly be a change in the scene, at which point we need to recalculate the number of rows and columns, recalculate the height of the container, which is essentially a reruninit
Functions; That’s why we need to listen inlist
;
watch: {
list: {
handler: function(newVal, oldVal) {
this.init();
},
immediate: true // Init is created only once, so init is not required when created}},Copy the code
handleMousedown()
Define handleMousemove() and handleMouseUp() events directly at handleMousedown() and remove them from handleMouseUp();
First, a few more important variables and methods
-
ItemList: a copy of the list with subsequent attributes dom (the node information of the current card, obtained by ref), isMoveing (whether the current card is moving), left, top,
-
CurItem: The current card is used a lot, so it is taken out separately, and the transition effect of the single front card should be removed when moving, otherwise the movement will stall, and the Z-index should be at a higher level
-
TargetItem: The card object for which positions are to be swapped, starting with null
-
MousePosition: the starting position of the mouse, minus the starting position after moving the mouse, is the card movement offset;
-
HandleMousemove () : Mouse movement
-
CardDetect () : detects card movement and whether position swapping needs to be performed
-
SwicthPosition () : Swap card position
-
HandleMouseUp () : Mouse lift
handleMousedown(e, optionItem) {
e.preventDefault();
let that = this;
if (this.timer) return false; // Timer indicates a global timer, indicating that a card is moving.
// Make a copy of the list and add the attributes to use later;
let itemList = that.list.map(item= > {
// if ref is a dynamically assigned value, $refs is an array;
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; // Mark moving cards. Moving cards do not participate in collision detection
return{... item, dom, left, top, isMoveing}; });// Save the current card object with an alias 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; // start left
curItem.startTop = curItem.top; // Start top
curItem.OffsetLeft = 0; // Left offset
curItem.OffsetTop = 0; // The offset of top
// The object whose position will be swapped
let targetItem = null;
// Record the starting mouse position
let mousePosition = {
startX: e.screenX,
startY: e.screenY
};
document.addEventListener("mousemove", handleMousemove);
document.addEventListener("mouseup", handleMouseUp);
// Mouse movement
function handleMousemove(e) {}
// Card swap detection
function cardDetect() {}
// Card swap
function swicthPosition() {}
// Mouse up
function handleMouseUp() {}}Copy the code
handleMousemove(e)
The current mouse coordinates minus the starting coordinates is the current card offset;
The card swap detection can be performed during the movement. In order to improve performance, the following throttling is done; 200ms once;
// Mouse movement
function handleMousemove(e) {
curItem.OffsetLeft = parseInt(e.screenX - mousePosition.startX);
curItem.OffsetTop = parseInt(e.screenY - mousePosition.startY);
// Change the style of the current card
curItem.dom.style.left = curItem.startLeft + curItem.OffsetLeft + 'px';
curItem.dom.style.top = curItem.startTop + curItem.OffsetTop + 'px';
// Check card switching, do throttling
if(! DectetTimer) { DectetTimer = setTimeout((a)= > {
cardDetect();
clearTimeout(DectetTimer);
DectetTimer = null;
}, 200)}}Copy the code
cardDetect()
The first idea is to do collision detection, loop through the itemList and compare the current card to each item; When less than the set gap, swicthPosition() is executed;
Behind the crack spring after the original article, found that the previous practice performance is too poor; Keep looping through the array;
According to the current position and offset, the target position targetItemDragCardIndex can be calculated. After judging some critical values, the exchange function will be executed.
// Card movement detection
function cardDetect() {
// Calculate which position to move to according to the distance moved
let colNum = Math.round((curItem.OffsetLeft / that.itemWidth));
let rowNum = Math.round((curItem.OffsetTop / that.itemHeight));
// dragCard_index needs to use the position where the card was originally clicked, because the curItem dragCard_index has changed in subsequent card exchanges;
let targetItemDragCardIndex = optionItem.dragCard_index + colNum + (rowNum * that.col);
// Return if the target position does not exist or does not exist;
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;
// Make a copy of the target card, mainly to assign the value to the current card when releasing the mouse;targetItem = {... item}; swicthPosition(); }Copy the code
swicthPosition()
There are two types of card exchanges;
- When the target position is larger than the original position of the current moving card, the separated card and the target card are moved back one position;
- When the target position is smaller than the original position of the current moving card, the separated card and the target card move forward one position;
: : : tip
- When we move, we take the previous value or the next value, so when we go through the array, be careful to start with the target value;
- ItemList is a backup of list. When we modify the dragCard_index of the card, we need to synchronize it to list.
- The card exchange animation is 300ms, during which time the card should not participate in the exchange detection, so set
isMoveing = true
And set a timer for 300ms before clearingisMoveing
- During the card exchange, the current card only needs to change
itemList
Properties in, do not need to changelist
In, wait until the last release of the mouse to synclist
: : :
function swicthPosition() {
const dragCardIndexList = itemList.map(item= > item.dragCard_index);
// The target card position is larger than the current card position;
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((a)= > {
item.isMoveing = false;
}, 300)}}// The target card position is smaller than the current card position;
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((a)= > {
item.isMoveing = false;
}, 300)
}
}
curItem.left = targetItem.left;
curItem.top = targetItem.top;
curItem.dragCard_index = targetItem.dragCard_index;
// Send the change event to notify the parent component
that.$emit('change', itemList.map(item= > item.dragCard_index));
}
Copy the code
handleMouseUp()
- When the mouse lifted should judge whether there is a target card, if so, to return to the target card, not to return to the initial position;
- [Fixed] Remove the transition effect from the current card when the mouse is clicked, add it back when the mouse is up. because
transition
incss
Is set in thestyle
Can be clear
function handleMouseUp() {
// Remove all listeners
document.removeEventListener("mousemove", handleMousemove);
document.removeEventListener("mouseup", handleMouseUp);
// Clear the detection timer and do the last collision detection
clearTimeout(DectetTimer);
DectetTimer = null;
cardDetect();
// Add the transition back
curItem.dom.style.transition = ' ';
// synchronize dragCard_index to 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';
// Send a mouseUp event to the parent component
that.$emit('mouseUp', that.list.map(item= > item.dragCard_index));
that.timer = setTimeout((a)= > {
curItem.dom.style.zIndex = ' ';
curItem.dom.childNodes[0].style.boxShadow = 'none';
clearTimeout(that.timer);
that.timer = null;
}, 300);
}
Copy the code
Write in the back
At this point the component is complete!
With me, implement and encapsulate drag-and-drop arrangement components from zero; This is a series of articles that will be shared later in Todo on how to upload components to NPM;
Address: github.com/Dranein/vue…