Drag and Drop with JavaScript

One of the greatest interface solutions of JavaScript is known as “Drag and Drop”. In this chapter, we are going to find out what it is mainly used for, and why it should be an essential part of your work.

Dragging and dropping something is a clear and straightforward way of doing multiple things: from copying and moving your documents to ordering ( for example, dropping an item into a cart).

Modern HTML standard includes a specific section with events like dragstart, dragend,and more. The handiest thing about these events is that they can help you solve simple tasks much easier. For example, with the help of them, you can handle the drag and drop of “external” files into your browser, in a way that you can take any file in the OS file manager and drop it into the browser window, thus giving JavaScript access to its contents.

However, there are limitations to native Drag events. For example, they don’t allow you to limit dragging by a certain area. Another limitation is that you can’t make it “vertical” or “horizontal” only. Other drag and drop tasks can’t be implemented by that API either.

Now, let’s see how to perform drag and drop with mouse events.

The Algorithm of Drag and Drop

The principal drag and drop algorithm looks as follows:

  • On the mousedown, you need to arrange the element for moving, if it is necessary ( for example, you can create its copy).
  • On the mousemove, you should move it by changing the left/top, as well as position:absolute.
  • On the mouseup, implement the overall actions connected to a finished drag and drop.

Now, you know the basics. Let’s get to the examples.

For instance, a drag and drop algorithm of a text will look like this:

<!DOCTYPE html> 
<html>
  <head>
    <title>Title of the Document</title>
  </head>
  <body>
    <p id = 'text'>Drag the text</p>
    <script>
      text.onmousedown = function(event) { //  start the process
      // get ready to move: make an absolute and top z-index
         text.style.position = 'absolute';
      text.style.zIndex = 1000;
      // move it from any existing parents directly to the body
      // to position it relative to the body
      document.body.append(text);
      // and put this absolutely positioned text under the pointer
      moveAt(event.pageX, event.pageY);
      // centers the text on the coordinates (pageX, pageY)
      function moveAt(pageX, pageY) {
        text.style.left = pageX - text.offsetWidth / 2 + 'px';
        text.style.top = pageY - text.offsetHeight / 2 + 'px';
      }      
      function onMouseMove(event) {
        moveAt(event.pageX, event.pageY);
      }
      
      //  move the text on mousemove
      document.addEventListener('mousemove', onMouseMove);
      
      // drop the text, remove unneeded handlers
      text.onmouseup = function() {
        document.removeEventListener('mousemove', onMouseMove);
        text.onmouseup = null;
      };      
      };      
    </script>
  </body>
</html>

After running the code above, you can notice a strange thing: at the start of the process, the text “forks”, and you begin dragging its clone. Such behavior shows up because the browser contains its own drag and drop for different elements such as images that run automatically and conflicting with yours.

So, for disabling it, you need to use the following code:

text.ondragstart = function () {
  return false;
};

There is another important aspect to note: you track mousemove on the document and not the text. But, mousemove triggers frequently, but not for each pixel. Hence, after the move, your pointer might jump from the text anywhere in the heart of the document., and outside of the window, as well.

Correct Positioning

As you noticed in the example above, the text always moves in a way that its center is under the pointer, like this:

text.style.left = pageX - text.offsetWidth / 2 + 'px';
tex.style.top = pageY - text.offsetHeight / 2 + 'px';

However, there is a side-effect here. For initiating the drag and drop, you should mousedown anywhere you want, on the text.

For example, if you begin to drag by the edge of the text, the pointer should be kept over the edge throughout the dragging.

You can also update the algorithm following the steps below:

  1. At the moment a visitor presses the mousedown button, save the distance from the pointer to the text’s left-upper corner in the variables shiftX/shiftY. That distance should be kept while dragging.

    Let’s subtract the coordinates to get these shifts, like here:

    // onmousedown
    let shiftX = event.clientX - text.getBoundingClientRect().left;
    let shiftY = event.clientY - text.getBoundingClientRect().top;
  2. As the next step, while dragging, position the text on the same shift that is relative to the pointer, as follows:
    // onmousemove
    // text has position:absolute
    text.style.left = event.pageX - shiftX + 'px';
    text.style.top = event.pageY - shiftY + 'px';

So, the final and better positioning is demonstrated below:

<!DOCTYPE html> 
<html>
  <head>
    <title>Title of the Document</title>
    <style>
      #text{
      cursor: pointer;
      cursor: pointer;
      width: 40px;
      height: 40px;
      }
    </style>
  </head>
  <body>
    <p id='text'>Drag the text</p>
    <script>
      text.onmousedown = function(event) {      
        let shiftX = event.clientX - text.getBoundingClientRect().left;
        let shiftY = event.clientY - text.getBoundingClientRect().top;      
        text.style.position = 'absolute';
        text.style.zIndex = 1000;
        document.body.append(text);      
        moveAt(event.pageX, event.pageY);      
        // move the text along the coordinates (pageX, pageY)
        // taking into account the initial shifts
        function moveAt(pageX, pageY) {
          text.style.left = pageX - shiftX + 'px';
          text.style.top = pageY - shiftY + 'px';
        }
      
        function onMouseMove(event) {
          moveAt(event.pageX, event.pageY);
        }
      
        // move the text to the  mousemove
        document.addEventListener('mousemove', onMouseMove);
      
        // drop the text, remove unneeded handlers
        text.onmouseup = function() {
          document.removeEventListener('mousemove', onMouseMove);
          text.onmouseup = null;
        };     
      };      
      text.ondragstart = function() {
        return false;
      };   
    </script>
  </body>
</html>

The d distinction between the past cases and this one is obvious: in the previous example, the text jumps under the pointer.

Potential Drop Targets (Droppables)

In the examples above, the text was dropped anywhere to stay. But, in practice, it is usually necessary to take an element and drop it into another (for example, a file into a folder).

The solution is a little tricky, but let’s cover it here.

There exists a document.elementFromPoint(clientX, clientY) method that returns the most nested element on particular window-relative coordinates (or null, in case the coordinates are located out of the window). It can be used in of the mouse event handlers for detecting the potential droppable under the pointer, as follows:

// mouse event handler
text.hidden = true; // (*)hide the element we are dragging
let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
// elemBelow - the element below the text, can be reset
text.hidden = false;

Please, take into consideration that you should hide the text before the call (*). In another way, you will have a text on that coordinates. So hide it, then immediately show again.

So, en extended code of onMouseMove used for finding droppable elements is here:

<!DOCTYPE html> 
<html>
  <head>
    <title>Title of the Document</title>
    <style>
      #text{
      cursor: pointer;
      cursor: pointer;
      width: 40px;
      height: 40px;
      }
    </style>
  </head>
  <body>
    <p id='text'>Drag the text</p>
    <script>
      let currentDroppable = null;
      text.onmousedown = function(event) {
        let shiftX = event.clientX - text.getBoundingClientRect().left;
        let shiftY = event.clientY - text.getBoundingClientRect().top;
      
        text.style.position = 'absolute';
        text.style.zIndex = 1000;
        document.body.append(text);      
        moveAt(event.pageX, event.pageY);      
        function moveAt(pageX, pageY) {
          text.style.left = pageX - shiftX + 'px';
          text.style.top = pageY - shiftY + 'px';
        }      
        function onMouseMove(event) {
          moveAt(event.pageX, event.pageY);      
          text.hidden = true;
          let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
          text.hidden = false;      
          if (!elemBelow) return;      
          let droppableBelow = elemBelow.closest('.droppable');
          if (currentDroppable != droppableBelow) {
            if (currentDroppable) { // null when we were not over a droppable before this event
              leaveDroppable(currentDroppable);
            }
            currentDroppable = droppableBelow;
            if (currentDroppable) { // null if we're not coming over a droppable now
              // (maybe just left the droppable)
              enterDroppable(currentDroppable);
            }
          }
        }      
        document.addEventListener('mousemove', onMouseMove);      
        text.onmouseup = function() {
          document.removeEventListener('mousemove', onMouseMove);
          text.onmouseup = null;
        };      
      };      
      function enterDroppable(elem) {
        elem.style.background = 'pink';
      }      
      function leaveDroppable(elem) {
        elem.style.background = '';
      }      
      text.ondragstart = function() {
        return false;
      };
    </script>
  </body>
</html>

Finally, there is a current drop target in the variable currentDroppable so that it can be used and highlighted.

Summary

In this chapter, we considered the basic algorithm for drag and drop. The interface of drag and drop allows applications to apply the drag and drop features on different browsers. The user can select a draggable element with the mouse, drag it to a droppable element, dropping it by releasing the mouse button.

For websites, extensions and so on, there is an option of customizing, which elements can become draggable, the type of feedback they produce. Also, there are frameworks building architecture on it: for example, DragZone, Droppable, Draggable, and other classes. Most of them act similarly to what was described above.




Do you find this helpful?

Related articles