Using HTML5 Drag and Drop

Apr 20, 2013

This article covers how to use the drag and drop capabilities of the HTML5 standard to allow dragging items on the page or files onto the web page.

We will start by going over the events, APIs, and related considerations then an example.

Drag and Drop Related Events

There are seven drag and drop related events, though you do not need to handle all of them for many cases.

  • dragstart
  • dragend
  • dragenter
  • dragover
  • dragleave
  • drop
  • drag

Making an Element Draggable

In order to make an element draggable you need to set the draggable attribute on the element and provide a handler for the dragstart event.

Drop Destinations

On the other end, to make an element a possible destination for a drag you must either set the dropzone attribute or set a handler for the dragenter event to report whether the drop will be accepted. Also, attach a listener to the drop event to accept the data when it is dropped, and listen to the dragover event in order to provide feedback to the user while an item is being drug.

The dragleave event is sent when the drag in progress leaves a potential target drop element.

The dragend event is sent with the original element as the target when the drag ends.

Listening to Events

In order to listen to an event for an element, the target, you register an event listener.

Here is an example that is rather more verbose than normal for clarity.

var target = document.getElementById("my-elem-id");
var eventName = 'dragend';
var eventHandlerFunc = function(evt) { alert("the drag ended"); };
var useCaptureFlag = false;
target.addEventListener(eventName, eventHandlerFunc, useCaptureFlag);

The use capture flag should normally be false and is now optional but was previously required in older browsers. A value of true would mean you want to receive the event in the capture and target fases instead of the bubbling and target phases. See the DOM event specification for more information on what it means.

DataTransfer

Drag and drop data is specified and sent using a DataTransfer object. It can contain multiple data items. Each data item has an associated MIME type and the actual data which is either a string or binary data from a file. It can co

To add data to the data transfer object, you use the setData() function.

On the receiving side you get the data from the object using the getData function by specifying the type of data you want.

The data transfer object also supports a setDragImage method which can be used to override the image which the user sees for dragging.

The data transfer object also has a couple important properties. The effectAllowed property is used to specifiy what is allowed, such as copy, move, link, etc.

If files are being drug, the data transfer object’s files property can be used to access the FileList object.

stopPropagation and preventDefault

The preventDefault method of an event object is used for event cancellation. This is used to prevent the default behavior for the event. Since the default behavior is not to allow drag and drop for most elements we must in fact prevent this default from happening.

The stopPropogation method of an event option stops the event from propogating or bubbling up the DOM. This means parents of the element will not also receive the event.

The File API

HTML5 also includes the File API which includes the FileList and FileReader objects. Using the FileReader we can access locally in the client from Javascript the actual contents of a file which has been drug to a drop location. This can be used to preview an image before upload, display a file, or whatever other use you can dream up.

Be aware that browsers may support drag and drop but not the File API so you must check for each separately.

When you receive a file drop the event’s target attribute will in turn have a ‘files’ attribute which is a FileList.

You can then create a FileReader object and read each of the files from the FileList using one of one of the methods: readAsArrayBuffer, readAsText, or readAsDataURL.

You must also attach an onload handlerr to the FileReader because the reading operations are asynchronous.

Putting it all together

Here we are going to setup a simple drag and drop demo that allows dragging some simple colored boxes between locations and also allows you to drag a file into a drop area and preview it if an image or text or view basic information if not.

The HTML Markup

<!DOCTYPE html>
<html lang="en">
<head>  
<meta http-equiv="Content-Type" tab-content="text/html; charset=UTF-8" />  
<title>Drag and Drop</title>
<link rel="stylesheet" href="../css/dnd.css" type="text/css"/>  
<script type="text/javascript" src="../js/jquery-1.8.1.min.js"></script>
<script type="text/javascript" src="../js/modernizr.custom.08307.js"></script>
<script type="text/javascript" src="../js/dnd.js"></script>
</head>  
<body>

<h1>HTML5 Drag and Drop Demo</h1>

<h2>Drag the numbers onto each other to change the order</h2>
<div id="numboxs">
    <div id="box1" class="numbox" draggable="true"><div id="inner1" class="inner">1</div></div>
    <div id="box2" class="numbox" draggable="true"><div id="inner2" class="inner">2</div></div>
    <div id="box3" class="numbox" draggable="true"><div id="inner3" class="inner">3</div></div>
</div>

<div class="clear">
</div>

<h2>Drop files here to see the contents</h2>
<div class="filedrop" draggable="true">
Drop your file here.
</div>


</body>

The CSS

.clear {
    float: none;
    clear: both;
}

.numbox {
    margin: 20px 10px;
    float: left;
    width: 150px;
    height: 125px;
    padding: 1px;
    cursor: move;
    -webkit-border-radius: 10px;
    -ms-border-radius: 10px;
    -moz-border-radius: 10px;
    border-radius: 10px;
}
.numbox.over {
  border: 1px dashed #000;
}

[draggable] {
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  user-select: none;
  /* Needed for old webkit case. */
  -khtml-user-drag: element;
  -webkit-user-drag: element;
}

#inner1  {
    background-color: red;
}
#inner2  {
    background-color: green;
}
#inner3  {
    background-color: blue;
}

.inner {
    height: 95px;
    text-align: center;
    font-size: 75px;
    padding: 15px;
    -webkit-border-radius: 10px;
    -ms-border-radius: 10px;
    -moz-border-radius: 10px;
    border-radius: 10px;
}

.filedrop {
     border: 1px solid #000;
    -webkit-border-radius: 10px;
    -ms-border-radius: 10px;
    -moz-border-radius: 10px;
    border-radius: 10px;
    background-color: #c0c0c0;
    margin: 10px;
    padding: 15px;
    min-height: 240px;
    width: 460px;
    overflow: auto;
    cursor: move;
}

.filedrop p {
    margin: 5px;
}

The Javascript Source

var doLog = false; // set to true to enable logging
// wrap calls to console.log in case not available or want to disable debugging output.
function myLog(msg)
{
    if (doLog && console && typeof console.log === "function") {
        console.log(msg);
    }
}

$(document).ready( function() {
    dndInit();
});

function dndInit() { 
    if (Modernizr.draganddrop) {
        myLog("Drag and drop supported");
        setupBoxes();
        if (window.File && window.FileReader && window.FileList) {
            myLog("File API supported");
            setupFileDrop();
        }
    }
}

function myFileBoxOver(e) {
    if (e.preventDefault) {
        e.preventDefault();
    }
    this.classList.add('over');
}
function myFileBoxLeave(e) {
    myLog("myFileBoxLeave");
    if (e.preventDefault) {
        e.preventDefault();
    }
    this.classList.remove('over');
}

function myFileHandler(e,file) {
    myLog("myFileHandler");
    myLog("file name: "+file.name);
    myLog("file type: "+file.type);
    myLog("file size: "+file.size);

    var reader = new FileReader();
    reader.onerror = function(e) {
        alert('There was an error reading the file: ' + e.target.error.code);
    };
    var target = e.target;
        $(target).empty();
    if (file.type.indexOf("text") == 0) {
        reader.onload = function(e) {
            myLog("text reader.onload");
            var text = e.target.result;
            $(target).text(e.target.result);
        }
        reader.readAsText(file);
    } else if(file.type.indexOf("image") == 0) {
        $(target).append('<img id="loaded-image"/>');
        reader.onload = function(e) {
            myLog("image reader.onload");
            $("#loaded-image").attr('src',e.target.result);
        }
        reader.readAsDataURL(file);
    } else {
        $(target).append("<p>file name: "+file.name+"</p>");
        $(target).append("<p>file type: "+file.type+"</p>");
        $(target).append("<p>file size: "+file.size+"</p>");
    }
}

function myFileDrop(e){
    if (e.stopPropagation) {
        e.stopPropagation();
    }
    if (e.preventDefault) {
        e.preventDefault();
    }
    this.classList.remove('over');
    myFileSelect(e);
    return false;
}

function myFileSelect(e) {
    // get FileList object
    var files = e.target.files || e.dataTransfer.files;
    // process all File objects
    for (var i = 0; files[i]; i++) {
        myFileHandler( e, files[i] ) ;
    }
}

function setupFileDrop() {
    // In real life you would show a fallback form if no drag and drop support.

    $(".filedrop").each( function() { 
        var fileBox = $(this).get()[0];

        fileBox.addEventListener("dragover",myFileBoxOver , false);
        fileBox.addEventListener("dragleave",myFileBoxLeave , false);
        fileBox.addEventListener("drop", myFileDrop, false);
        fileBox.style.display = "block";
        fileBox.addEventListener('dragenter', myDragEnter, false);
        fileBox.addEventListener('dragleave', myDragLeave, false);
    });
}

function myDragOver(e) {
    if (e.preventDefault) {
        e.preventDefault();
    }
    e.dataTransfer.dropEffect = 'move';  
    return false;
}

function myDragEnter(e) {
    myLog("my enter");
    this.classList.add('over');
    if (e.preventDefault) {
        e.preventDefault();
    }
    return true;
}

function myDragLeave(e) {
    myLog("my leave");
    this.classList.remove('over');
}

function myDrop(e) {
    myLog("my drop");
    if (e.stopPropagation) {
        e.stopPropagation();
    }
    if (e.preventDefault) {
        e.preventDefault();
    }

    var srcId = e.dataTransfer.getData('text/plain');
    if (srcId != this.id) {
        myLog("Drug! srcId="+srcId);
        var src = document.getElementById(srcId);
        if(src) {
            myLog("swapping");
            // swap contents
            var myHTML = this.innerHTML;
            this.innerHTML = src.innerHTML;
            src.innerHTML = myHTML;
        }
    }

    myLog("myDrop over");
    return false;
}

function myDrag(e) {
    // do things here if you need to while dragging
}
function myDragEnd(e) {
    myLog("my drag end");

    this.style.opacity = '1.0';
    $(".numbox").each( function() { 
        this.classList.remove('over');
    });
}

function myDragStart(ev) {
    myLog("my start");
    this.style.opacity = '0.4';
    ev.dataTransfer.effectAllowed='move';
    var textData = ev.target.getAttribute('id');
    myLog("textData="+textData);
    ev.dataTransfer.setData("text/plain", textData);
    return true; 
}

function setupBoxes() {
    $(".numbox").each( function() { 
        var elem = $(this).get()[0];
        myLog("draggable id " + elem.id);
        elem.addEventListener('dragstart', myDragStart, false);
        elem.addEventListener('dragend', myDragEnd, false);

        elem.addEventListener('dragenter', myDragEnter, false);
        elem.addEventListener('dragover', myDragOver, false);
        elem.addEventListener('dragleave', myDragLeave, false);

        elem.addEventListener('drop', myDrop, false);
        elem.addEventListener('drag', myDrag, false);
    });
}

All Together

Live Demo

References

Here are some links to libraries we used, specificiations, and other related information.