Create a Simple HTML5 Drag and Drop Interface

Difficulty : Intermediate


Drag and drop interfaces are extremely useful tools in any graphical user interface. They help use quickly create/rearrange layouts, manage multiple files, group similar items together and a multitude of other functions. Today we’ll look at making a simple drag and drop interface with the HTML5 Drag and Drop API.

View live demo

Note: Although browser support is fairly decent across the board there are certain methods that lack any support on IE at all. These methods don’t break the basic functionality of the interface but without them it is slightly difficult to override default browser settings. I’ll make note of these methods as we go along.

The HTML


First, we will need to define which elements will be draggable and which elements act as drop zones to accept draggable elements. I’ll be using custom data attributes to accomplish this.

<div class="dropzones" >
    <div data-state="drop"></div>
    <div data-state="drop"></div>
    <div data-state="drop" style="flex: 1 1 100%"></div>
    <div data-state="drop"></div>
    <div data-state="drop"></div>
</div>

<div class="draggables">
    <div data-state="drag" data-method="move">Move Me!</div>
    <div data-state="drag" data-method="copy">Copy Me!</div>
</div>

In addition to the data-state attribute, I also added a data-method attribute. This will decide what action to take when an element is dropped.

The CSS


.center-content {
    background: rgba(230,230,230,1);
    display: flex;
    flex-flow: row wrap;
    justify-content: space-between;
    align-items: center;
}

.center-content > * {
    flex: 0 1 49%;
}

.dropzones {
    display: flex;
    flex-flow: row wrap;
    justify-content: space-between;
    align-items: center;
    margin: 0 auto;
}

*[data-state="drop"] {
    min-height: 215px;
    color: #fff;
    box-sizing: border-box;
    background: rgba(150,150,150,.1);
    border: 4px solid rgba(200,200,200,1);
    border-radius: 7px;
    margin-bottom: 10px;
    padding: 15px;
    flex: 0 1 49%;
    display: flex;
    flex-flow: column wrap;
    justify-content: center;
    align-items: center;
    position: relative;
}

*[data-state="drag"] {
    cursor: pointer;
    display: inline-block;
    font-family: sans-serif;
    padding: 25px;
    margin-bottom: 15px;
    background: rgba(200,200,200,1);
    color: rgb(79, 79, 79);
}

    *[data-state="drag"],
    *[data-state="drop"] {
    -webkit-touch-callout: none; /* iOS Safari */
    -webkit-user-select: none; /* Safari */
    -khtml-user-select: none; /* Konqueror HTML */
    -moz-user-select: none; /* Firefox */
    -ms-user-select: none; /* Internet Explorer/Edge */
    user-select: none;
}

*[data-state="drag"] > * {
    pointer-events: none;
}

There are two important CSS properties to note here: pointer-events and user-select.

The pointer-events property disables all mouse events on an element. This is needed to prevent child elements within the draggable parent from firing off unwanted drag events.

The second property, user-select, makes it so that drop zones can’t be selected and dragged into themselves or other drop zones.

The Javascript


Now for the meat and potatoes.

We’ll break this down in sections since there’s a fair amount of javascript to write.

Initialize global variables

First let’s set up some variables that will be used throughout the script.

let draggables = document.querySelectorAll('*[data-state="drag"]');
let dropzones = document.querySelectorAll('*[data-state="drop"]');
let isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
let dragkey;
let dragging = false;

I made two variables to hold all draggable elements and drop zones respectively.  I then created 3 utility variables to use inside conditional statements:

  • isIE11 – used to run IE (or non-IE) specific code
  • dragkey – the index for the currently dragged element
  • dragging – a boolean flag to check if the user is currently dragging an element

Attach event listeners

Now that we have handles for all our elements, let’s begin writing our event listeners.

We’ll start with the draggables:

Object.keys(draggables).forEach(function(key) {
        draggables[key].addEventListener('mousedown', function(evt) {
        this.draggable = true;
    });

    draggables[key].addEventListener('dragstart', function(evt) {
    let dt = evt.dataTransfer;

    if (!isIE11) {
        dt.setDragImage(new Image(), 0, 0); // Remove ghost image from cursor
        dt.setData('key', '' + key); // Initiate drag events in Firefox
    }

        dragkey = key; // grab index of current element
        dragging = true;
    });

    draggables[key].addEventListener('drag', function(evt) {
        dragging = true;
    });

    draggables[key].addEventListener('dragend', function(evt) {
        this.draggable = false;
        dragging = false;
    });
});

mousedown

I set the draggable flag we declare above to true. Later on in both the dragend and drop events I switch it back to false since by the point the user should no longer be dragging an element. I also set the draggable attribute to true to allow the element to be dragged (if not so by default).

dragstart 

Here I use a special property of the drag event known as the dataTransfer object. Here you can pass several types of data along with the drag event using the setData method and retrieve them with it’s counterpart getData inside the drop event. I’m also binding the dragkey to the key within the forEach loop. This will be used to retrieve the dragged element later on in the drop event.

As I stated in the beginning, both setData and getData are not supported in IE or Edge. Also you must set some kind of data with setData in Firefox to fire off the dragstart event.

One last gotcha — the default browser behavior for dragged elements is to attach a ghosted image of whatever is being dragged to the cursor (if the element exceeds 300px in width and height the outer edges are heavily faded). You can disable this by using the setDragImage method and passing an empty image element. However this is also another method that is not supported by IE/Edge. You can temporarily hide the dragged image then bring it back with a setTimeout to force IE/Edge to behave the same but this causing a noticeable flicker effect which may be undesirable to some.

drag

Here you can execute any code you’d like while a element is being dragged. I decided to keep setting the dragging flag to true since at this point the user would be performing a drag action.

dragend

This will fire once a user has let go of a dragged element (typically on mouse up). It runs concurrently with the drop event when a draggable is dropped into a valid drop zone. It also runs independent of any drop events, therefore you could also use dragend as a cancellation event when no drop event is fired.

I reset the dragging flag and disabled dragging on the element.

Next up the drop zones:

Object.keys(dropzones).forEach(function(key) {
    // Disable drag if true by default
    dropzones[key].draggable = false;

    dropzones[key].addEventListener('dragenter', function(evt) {
        // enables dropzones when dragging
        if(dragging && evt.target.dataset.state === 'drop') {
            evt.preventDefault();
            evt.target.style.borderStyle = "dashed";
            evt.target.style.background = "rgba(200,200,200,.1)";
        }
    });

    dropzones[key].addEventListener('dragover', function(evt) {
        // enables dropzones when dragging
        if(dragging && evt.target.dataset.state === 'drop') {
            evt.preventDefault();
        }
    });

    dropzones[key].addEventListener('dragleave', function(evt) {
        evt.target.style.borderStyle = "";
        evt.target.style.background = "";
    });

    dropzones[key].addEventListener('drop', function(evt) {
        let dragged = draggables[dragkey];
        dragged.draggable = false;
        dragging = false;

        switch(dragged.dataset.method) {
            case 'move':
            this.appendChild(dragged);
            break;

            case 'copy':
            let clone = dragged.cloneNode(true);
            clone.draggable = true;
            this.appendChild(clone)
            break;
        }

        this.style.borderStyle = "";
        evt.target.style.background = "";
    });
});

First we’ll disable the draggable attribute on all drop zones (if you need to have your drop zones also be draggable you can skip this).

dragenter

Here I listen for when a dragged element first enters a drop zone. If our dragging flag is true and the event target (the drop zone) has a data-state of “drop” then prevent the default behavior.

Why prevent default? Because most HTML elements are not draggable by default dragenter and dragover will not listen out for any dragged elements. In order to create a valid drop zone we need to prevent this default behavior and listen for anything that enters the drop zone. To prevent any unwanted drag elements from being accepted I set up a conditional using our dragging flag and the data-state attribute. If both of these match then we proceed with executing our code on hover.

The rest is just changing the style of the drop zone so the user can visually see where a dragged element is allowed to drop.

dragover

Same deal as dragenter. The main difference between the two is that dragover will fire every few hundred milliseconds.

dragleave

The opposite of dragenter. I reset the styles from earlier to let the user know they have left the drop zone.

drop

Once a user releases a dragged element over a drop zone they will initiate a drop event. Here’s where the magic happens.

I take the dragkey we set earlier in the drag events and use it to create a handle for the element the user was just dragging. From there we can use either the keyword this or the target property in the event object to reference our drop zone.

I set up a switch statement to grab the value of data-method and perform different actions depending on which method is set. For the move method I simply append the dragged element to the current drop zone. For the copy method I make a deep clone using the cloneNode method and append that to the drop zone instead, leaving the original intact.

Once everything is complete I reset the style of the drop zone to signal to the user that the drop was successful.

That’s it.

We now have a usable drag and drop interface! 😀

If you enjoyed this tutorial make sure to share or leave a comment below. If you have any examples of how you’ve used this we’d love to see that too!

Categorised in: Uncategorized

This post was written by Ajay Alkondon

Leave a Reply

Your email address will not be published. Required fields are marked *