The final result

Native implementation principles

About the drag and drop

The image of the tag can be dragged by default.

However, other tags (divs, etc.) cannot be dragged. Mouse click and move will not have drag effect. Add attribute draggable=”true” to allow elements to be dragged.

The drag and drop process triggers an event

  • Trigger an event on a drag target (source element) :

    • Ondragstart – Triggered when the user starts dragging elements
  • Ondrag – Triggered while the element is dragging
  • Ondragend – Triggered when the user finishes dragging the element
  • Events that trigger when a target is released:

    • Ondragenter – This event is triggered when a mouse-dragged object enters its container scope
  • Ondragover – This event is triggered when a dragged object is dragged within the scope of another object container
  • Ondragleave – This event is triggered when an object dragged by the mouse leaves its container scope
  • Ondrop – This event is triggered when the mouse key is released during a drag

Ondragend and ondragOver Reset the current drag operation to “None “. That is, its default line is that when onDragover is triggered, the movement will fail (creating a residual), so to make an element placement, you need to prevent the default behavior.

element.ondragover = event= > {      
    event.preventDefault();     // ... 
}
Copy the code

A simple JS native drag demo

Thinking about thinking

ondragstart

Get the source element

ondrag

Source element style changes during drag and drop

ondragend

Remove excess styles, get the last target element index in onDragover, and change the list array

ondragover

In a short period of time to generate a snapshot, record the source element drag track through which elements, used to generate the final target element, to the passing element to add displacement animation style

Problem solving

How is data transferred

Idea 1: for each item the className = ${index}, the index by Number (event. The target. The classList. Slice (” 5 “) to the values,

Can achieve the desired effect, but the implementation is not elegant, so abandon this idea

Idea 2: the attribute data – account for each item index = {index}, through the event. The target. The dataset. The index value, conforms to the semantic code, the method is adopted

Nested DOM causes onDragover to fire repeatedly

The onDragover function fires repeatedly when the source element moves within the target element without stopping the drag. If the source element is not a single-layer DOM structure, each DOM layer fires ondragover, causing data chaos

The first red box in the console below is printed when item1 is dragged, and the second red box is printed when Item2 is dragged. You can see that the two-layer DOM structure of Item1 triggers two prints:

<! DOCTYPEhtml>

<html lang="zh">
  <head>
    <meta charset="UTF-8" />

    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />

    <meta http-equiv="X-UA-Compatible" content="ie=edge" />

    <title>Document</title>

    <style>
      .wrapper {
        display: flex;
        border: 1px solid orangered;
        padding: 10px;
      }

      .col {
        border: 1px solid # 808080;
        height: 500px;
        width: 200px;
        margin: 0 10px;
        padding: 10px;
      }

      .item {
        border: 1px solid # 808080;
        margin: 5px 0;
      }
    </style>
  </head>

  <body>
    <div class="wrapper">
      <div class="col1 col">
        <div class="item" id="item1" draggable="true">
          <div>item1</div>
        </div>

        <div class="item" id="item2" draggable="true">item2</div>
      </div>
    </div>

    <script>
      let cols = document.getElementsByClassName("col");

      for (let col of cols) {
        col.ondragover = (e) = > {
          e.preventDefault();

          console.log(e.target);
        };
      }
    </script>
  </body>
</html>

Copy the code

The solution to this problem is to stop using E.turrenttarget and use E.currenttarget instead

Concepts for target and currentTarget:

1. Target occurs in the target phase of the event flow, while currentTarget occurs in the entire phase of the event flow (capture, target, and bubble phases)

This is true only when the target flow is in the target phase.

3. While the event stream is in the capture and bubble phases, target points to the clicked object and currentTarget points to the current event active object, usually the ancestor of the event.

Double shadow appears after drag ends

The so-called double shadow is when we drag to the specified position, the page element we drag first slowly drift to the initial position, and then update to the position we specify

The solution is to add methods on top of the parent element

onDragOver``={(e:) => {

e.``preventDefault``();

}} >

Disable default drag behavior (drift slowly to the initial position is the default drag behavior is disabled)

Appropriate drag and drop transition animation

Make the picture appear to move up or down “give way” effect

.drag-up {
  animation: dragup ease 0.1 s 1;

  animation-fill-mode: forwards;
}

.drag-down {
  animation: dragdown ease 0.1 s 1;

  animation-fill-mode: forwards;
}

@keyframes dragup {
  from {
    margin-top: 10px;
  }

  to {
    margin-top: 50px; }}@keyframes dragdown {
  from {
    margin-bottom: 10px;

    margin-top: 50px;
  }

  to {
    margin-bottom: 50px;

    margin-top: 10px; }}Copy the code

Other Third-party libraries

The name of the react-dnd react-beautiful-dnd react-sortable-hoc
Package volume larger Large (> 100 k) smaller
TS support There are There is no There is no
Maintenance is good good Not updated for more than a year
By reference event.target.dataset event.target.dataset ref
Use the difficulty Higher learning costs The cost of learning is low The cost of learning is low
The core to realize html5 drag API html5 drag API html5 mouse API
The warehouse address Github.com/react-dnd/r… Github.com/atlassian/r… Github.com/clauderic/r…

In fact, except for the last library, the other two well-known libraries are not very small, so the simple requirement of native implementation is still necessary

react-dnd

Codesandbox. IO/s/a lot/re… (Experience demo)

React DnD builds on the HTML5 Drag and Drop API. Because it can take a screenshot of a dragged DOM node and use it as a “drag preview” so you don’t have to do anything drawing while the cursor is moving, the API is also the only way to handle file deletion events.

However, the HTML5 drag-and-drop API has some drawbacks. It doesn’t work on touch screens, and it offers less customization on IE than other browsers.

This is why the React DnD implementation of the HTML5 Drag and Drop API is pluggable. You don’t have to use it, you can wrap your implementation around native touch events, mouse events, etc. This pluggable implementation is called Backends in React DnD. The library is currently only used in HTML Backend, but may be used in more places in the future.

Backends functions like the React integrated event system: they both abstract away browser differences and handle local DOM events. Unlike React, Backends does not rely on React or React’s integrated event system. In the underlying implementation, what Backends does is convert native Dom events into internal Redux actions that React DnD can handle.

react-beautiful-dnd

React-beautiful-dnd.net lify. App/path = / stor… (Experience demo)

react-sortable-hoc

Clauderic. Making. IO/react – sorta… (Experience demo)

The core code

const [dragged, setDragged] = useState<any> ();const [over, setOver] = useState<any> ();const [draggable, setDraggable] = useState(false);

const dragStart = (e: any) = > {
  e.currentTarget.style.backgroundColor = "#fafafa";

  setDragged(e.currentTarget);
};

const dragEnd = (e: any) = > {
  e.preventDefault();

  e.target.style.display = "flex";

  e.target.classList.remove("drag-up");

  over.classList.remove("drag-up");

  e.target.classList.remove("drag-down");

  over.classList.remove("drag-down");

  const from = cloneDeep(value[dragged.dataset.index]);

  const to = cloneDeep(value[over.dataset.index]);

  splice(dragged.dataset.index, 1, to);

  splice(over.dataset.index, 1.from);

  e.target.style.opacity = "1";

  e.target.style.backgroundColor = "";
};

const dragOver = (e: any) = > {
  e.preventDefault();

  const dgIndex = dragged.dataset.index;

  const taIndex = e.currentTarget.dataset.index;

  const animateName = dgIndex > taIndex ? "drag-up" : "drag-down";

  if(over && e.currentTarget.dataset.index ! == over.dataset.index) { over.classList.remove("drag-up"."drag-down");
  }

  if (!e.currentTarget.classList.contains(animateName)) {
    e.currentTarget.classList.add(animateName);

    setOver(e.currentTarget);
  }
};

Copy the code
{items.map((item, index) = > {
  return (<Item
   draggable={draggable}
   data-index={index}
   onDragStart={e= >{ dragStart(e); }} onDrag={(e: any) => { e.preventDefault(); e.target.style.opacity = '0'; }} onDragOver={dragOver} onDragEnd={e => { dragEnd(e); }}> // Your list array data<Item/>)}Copy the code