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 usingabsoluteLayout by settingleftandtop, let the cards be in order, so incominglistIt must be in positive order;
  • Initialize the style, passpropsIncoming 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 boundmousemoveandmouseupEvents; 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’schangeMethods;
  • When the mouse is released, the card returns to the target position, triggering the parent component’smouseUpMethods;

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@mousedownSet in thedragCard_headIn order to achieve this, theslotIt’s divided into two parts. One isheadThe title section, displayed by defaultitem.content; One is thecontentThe content section is displayed by defaultitem.head,; Users can useslotCustom 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, solistThere 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 reruninitFunctions; 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

  1. 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;
  2. ItemList is a backup of list. When we modify the dragCard_index of the card, we need to synchronize it to list.
  3. The card exchange animation is 300ms, during which time the card should not participate in the exchange detection, so setisMoveing = trueAnd set a timer for 300ms before clearingisMoveing
  4. During the card exchange, the current card only needs to changeitemListProperties in, do not need to changelistIn, 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. becausetransitionincssIs set in thestyleCan 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;

[email protected]

Address: github.com/Dranein/vue…