Advertisement
HTML5

Implementing HTML5 Drag and Drop

by

One of the new features in HTML5 is native drag and drop. Surprisingly, Internet Explorer has had support for native drag and drop since version 5.5; in fact, the HTML5 implementation is based on IE's support. In today's tutorial, we'll look at how to implement native drag and drop to build a simple shopping cart interface.


Step 0. What We're Doing

Here's what we're going to build: it's a basic shopping cart with a product panel and a cart panel. To "purchase" a product, you'll be able to drag it from the panel to the cart; we'll keep a track of quantity and remove items from the product panel when they're out of stock.

Note that we're not actually building a shopping cart here; we won't be working with any server-side goodness today. This is just the front-end; the point is HTML5 drag and drop.


Step 1. The HTML

Of course, we'll start with the HTML; here's our shell:

 
<!DOCTYPE html> 
<html> 
<head> 
    <meta charset="utf-8" /> 
    <title>Drag and Drop Shopping Cart</title> 
    <link rel="stylesheet" href="default.css" /> 
</head> 
<body> 
 
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> 
    <script src="jquery.ndd.js"></script> 
    <script src="dragdrop.js"></script> 
</body> 
</html>

Pretty basic: we're linking to a stylesheet and jQuery; we're only using jQuery for easy event handling and DOM manipulation; the drag and drop will be native. However, we're up against a wall here, because HTML5 drag and drop adds a few properties to the event object. JQuery doesn't use the default event object; it creates its own to equalize event object issues. Because of this, we don't get the special properties with jQuery's event object. But don't worry; there's a plugin for that; we're pulling in the Native Drag and Drop to make it all work. Finally, we'll include our script: dragdrop.js.

Now we're ready to add in our product list; for product images, I'm using icons from the Apple Icon Superpack, created by SvenVath. (I have renamed the icons with simpler names and resized them to 180px.)

Add ul#products as the first child inside the body. Once you've done that, we'll examine the first list item:

 
<li><a class="item" href="#" id="imac" draggable="true"> 
    <img src="http://tutsplus.s3.amazonaws.com/tutspremium/web-development/064_drag_drop_html5/images/imac.png" /> 
    <div> 
        <p><strong>iMac</strong></p> 
        <p><strong>Price</strong>: <span>$1199.00</span></p> 
        <p><strong>Quantity</strong>: <span>10</span></p> 
    </div> 
</a></li>

We've got out list item, with an anchor inside; notice that each anchor item will have a class of item (important when we get to the CSS) and a custom id (important when we get to the JavaScript). Then, the anchor also has the draggable="true" attribute; this should be all we need to make the element draggable (we'll see a few caveats soon). We're using an anchor tag here so you could do something for browsers without native drag and drop support (although we won't be doing that here). Then we have the product image and a div with the product info. And yes, it's necessary to wrap the price and quantity with span

Here are the rest of the list items:

 
<li><a class="item" href="#" id="iphone" draggable="true"> 
    <img src="http://tutsplus.s3.amazonaws.com/tutspremium/web-development/064_drag_drop_html5/images/iphone.png" /> 
    <div> 
        <p><strong>iPhone</strong></p> 
        <p><strong>Price</strong>: <span>$199.00</span></p> 
        <p><strong>Quantity</strong>: <span>16</span></p> 
    </div> 
</a></li> 
<li><a class="item" href="#" id="appletv" draggable="true"> 
    <img src="http://tutsplus.s3.amazonaws.com/tutspremium/web-development/064_drag_drop_html5/images/appletv.png" /> 
    <div> 
        <p><strong>AppleTV</strong></p> 
        <p><strong>Price</strong>: <span>$299.00</span></p> 
        <p><strong>Quantity</strong>: <span>9</span></p> 
    </div> 
</a></li> 
<li><a class="item" href="#" id="dislpay" draggable="true"> 
    <img src="http://tutsplus.s3.amazonaws.com/tutspremium/web-development/064_drag_drop_html5/images/display.png" /> 
    <div> 
        <p><strong>Cinema Display</strong></p> 
        <p><strong>Price</strong>: <span>$899.00</span></p> 
        <p><strong>Quantity</strong>: <span>4</span></p> 
    </div> 
</a></li> 
<li><a class="item" href="#" id="nano" draggable="true"> 
    <img src="http://tutsplus.s3.amazonaws.com/tutspremium/web-development/064_drag_drop_html5/images/nano.png" /> 
    <div> 
        <p><strong>iPod Nano</strong></p> 
        <p><strong>Price</strong>: <span>$149.00</span></p> 
        <p><strong>Quantity</strong>: <span>20</span></p> 
    </div> 
</a></li> 
<li><a class="item" href="#" id="macbook" draggable="true"> 
    <img src="http://tutsplus.s3.amazonaws.com/tutspremium/web-development/064_drag_drop_html5/images/macbook.png" /> 
    <div> 
        <p><strong>Macbook</strong></p> 
        <p><strong>Price</strong>: <span>$1199.00</span></p> 
        <p><strong>Quantity</strong>: <span>13</span></p> 
    </div> 
</a></li> 
<li><a class="item" href="#" id="mini" draggable="true"> 
    <img src="http://tutsplus.s3.amazonaws.com/tutspremium/web-development/064_drag_drop_html5/images/mini.png" /> 
    <div> 
        <p><strong>Mac Mini</strong></p> 
        <p><strong>Price</strong>: <span>$599.00</span></p> 
        <p><strong>Quantity</strong>: <span>18</span></p> 
    </div> 
</a></li>

There's one final piece of HTML: the shopping cart:

 
<div id="cart"> 
    <h1>Shopping Cart</h1> 
    <ul></ul> 
    <p id="total"><strong>Total:</strong> $<span>0.00</span></p> 
    <hr /><h2>Drop here to add to cart</h2> 
</div>

And that's our HTML!


Full Screencast



Step 2. The CSS

Ideally, all you should need to do to make an element draggable is set that draggable attribute to true; however, there's more to it. To get things working properly, you have to set a few things in CSS. First, think of this: what does clicking and dragging do on "normal" (undraggable) element? Usually, it selects text. Then, we want to make sure we're dragging the element, and not just its contents. To deal with with, need to use the following CSS:

 
[draggable=true] { 
    -moz-user-select:none; 
    -webkit-user-select: none; 
    -webkit-user-drag: element; 
}

For our convenience, the native drag and drop plugin we're using sets these properties for us, so we can leave them out if we want. However, we will do this:

 
[draggable=true] { 
    cursor : move; 
}

Let's start styling:

 
html { 
    height:100%; 
} 
body { 
    background: #ececec; 
    margin:0; 
    padding:0; 
    font: 13px/1.5 helvetica, arial, san-serif; 
    height: 100%; 
} 
h1, h2 { text-align:center; } 
h2 { 
    position:absolute; 
    bottom: 20px; 
    color:#fff; 
    text-shadow:0 0 10px rgba(0, 0, 0, 0.75); 
    display:none; 
} 
p { margin:0; }

Since we're not using a full reset, here's our pseudo-reset. It should all be pretty self-explanetory. We're setting the height to 100% on the html and body elements because we want #cart to be the entire height of the screen; to do that, every parent element needs to have its height set to 100%. Also, notice that we're using rgba to set the shadow color; if a browser doesn't support this, there won't be a shadow on the h2. And, we're hiding this h2 by setting display: none. Remember, the h2 says "Drop here to add to cart," so we'll have it fade in when the drag starts and fade out when the drag ends.

Moving on to our products list . . .

 
#products { 
    float:left; 
    list-style:none; 
    width:65%; 
    padding:0; 
} 
#products li { 
    display:inline; 
}

Again, pretty obvious. The important thing in this snippet is that the list items will be displayed inline. Since we'll set display:block; float:left on the anchors, IE will give the list items a "stair-steps" effect; we can work around this bug by setting display: inline on the anchor's parent element.

Speaking of the anchors, let's style them next.

 
.item { 
    display:block; 
    float:left; 
    width:180px; 
    height:180px; 
    margin:10px; 
    border:1px solid #494949; 
    text-align:center; 
    text-decoration:none; 
    color: #000; 
    overflow:hidden; 
} 
.item img { 
    border:0; 
    margin:10px auto; 
    width:160px; 
    height:160px; 
} 
.item div { 
    background:rgb(0, 0, 0); 
    background: rgba(0, 0, 0, 0.5); 
    position:relative; 
    bottom:69px; 
    color:#f3f3f3; 
    padding:5px 0; 
    display:none; 
}

Each anchor will be styled as a 180x180px box; this will be filled with the product image. The product info div will be positioned over the image, at the bottom of the square. Notice we have to set a background color and then reset it for modern browsers, because IE doesn't support RGBa. We're setting display:none on this info div so we can fade it in and out when the "customer" hovers on and off, respectively.

All that's left to style is the shopping cart; we'll look at it here, but, remember, the list items will be inserted by jQuery later on, so you won't see this taking place just yet.

 
#cart { 
    float:right; 
    background-color:#ccc; 
    width:25%; 
    padding:0 5%; 
    height:100%; 
} 
#cart ul { 
    padding:0; 
} 
#cart li { 
    list-style:none; 
    border-bottom:1px solid #494949; 
    padding:5px; 
} 
#cart .quantity { 
    font-weight:bold; 
    padding-right:10px; 
    margin-right:10px; 
    border-right:1px solid #494949; 
    display:inline-block; 
    width:15px; 
    text-align:right; 
} 
#cart .price { 
    float:right; 
} 
#total { 
    float:right; 
}

Elements with the classes quantity and price will be within the dynamically inserted list items.

That's it for CSS; before moving on to the star of this show, let's look at our work so far.



Step 3. The JavaScript

We've made it to the JavaScript; according to the the HTML5 doctor:

HTML 5 DnD is based on Microsoft’s original implementation which was available as early as Internet Explorer 5! Now currently supported in IE, Firefox 3.5 and Safari 4.

Here's a list of the events that HTML5 drag and drop offers:

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

We won't use all of these, but we'll see how they most of them work.

First, we'll work with our products:

 
$('.item') 
    .bind('dragstart', function (evt) { 
        evt.dataTransfer.setData('text', this.id); 
        $('h2').fadeIn('fast'); 
    }) 
    .hover( 
        function () { $('div', this).fadeIn(); },  
        function () { $('div', this).fadeOut(); } 
    );

We start by grabbing all the items; then, we bind a funciton to the dragstart event; this event will fire when we start dragging the event. The first thing we'll do when an object is dragged is set some data; actually, if no data is set, firefox won't let the element drag. Special to drag events is a object property on the event object called dataTransfer; we'll be using two methods of this property: setData and getData. Here, we're using the setData method, which takes two parameters: a data format and the data. We'll use the datatype 'text.' Then, we'll set the data to be the id of the element the user is dragging. Then, we'll fade in the h2 as an prompt to the customer.

We also use the jQuery hover method to fade in the product info when we mouseover and fade it out when we mouseout. Notice that we pass the node we're hovering over as context, so we only get the div of the appropriate product.

Now let's add the event handlers to the #cart. We're going to act on the dragover, dragenter, and drop events:

 
$('#cart') 
    .bind('dragover', function (evt) { 
        evt.preventDefault(); 
    }) 
    .bind('dragenter', function (evt) { 
        evt.preventDefault(); 
    }) 
    .bind('drop', function (evt) { 
 
    });

To get the drop event to fire, we need to cancel the default action on the dragover event; this event fires continuously on the drop target when a draggable element is dragged over it. For IE only, we need to cancel the default action on the dragenter event, which occurs only when the draggable element enters the drop target. The reasons behind cancelling the default action are somewhat foggy; to be honest, I don't really understand them. Here's what Remy Sharp says about it:

What it’s telling the browser is that this element is the one we want to catch the drag event against, otherwise the browser goes ahead does it’s normal drag action. So by cancelling the event, it tells the browser that this is the element that should start moving.

I should note that jQuery is helping us out a bit here; normally, we'd also have to return false it get it working in IE; however, jQuery's fixed preventDefault does that for us.

Now the drop event; this event, too, is fired on the drop target, which is div#cart in our case. Before we look at the function, let's talk about what this function is supposed to do:

  • get the product we've dropped
  • if the product has already been bought, add one to the quantity purchased; otherwise, add the item to the cart
  • decrement the product quantity
  • update the total price

Here's the first part:

 
var id = evt.dataTransfer.getData('text'), 
    item = $('#' + id), 
    cartList = $("#cart ul"), 
    total = $("#total span"), 
    price = $('p:eq(1) span', item).text(), 
    prevCartItem = null, 
    notInCart = (function () { 
        var lis = $('li', cartList), 
            len = lis.length, 
            i; 
 
        for (i = 0; i < len; i++ ) { 
            var temp = $(lis[i]); 
            if (temp.data("id") === id) { 
                prevCartItem = temp; 
                return false; 
            } 
        } 
        return true; 
    } ()), 
    quantLeftEl, quantBoughtEl, quantLeft;

I know; it's a lot of variables, but we'll use them all. Let's go over them; first, we get the text data that we transferred with the event; we transferred the id like this because there's nothing in the event object to tell us which element was dropped onto our target; we'll get the id and then use it to find the element that was dropped. Next, we get the cart list and the total price span. Then, we'll get the price of the individual item; we know that it's the span within the second paragraph of the item, so we can use the item as the context parameter. We'll set prevCartItem to null for now, but we'll use it to see if the item dragged into the cart is already there. The value of notInCart is a self-invoking anonymous function; it will loop over each list item in the cartList (again, we're using the context parameter) and check to see if the data property id is the same as the variable id. To understand this, you'll have to know that when we add list items to the shopping cart, we'll use the jQuery data method to set store product id with the item. In this function, we're checking for a list item with the right data; if we find one, the item is already in the cart, and so we set notInCart to false; if it isn't in the cart, we'll set the variable to true. Finally, we'll use quantLeftEl, quantBoughtEl, and quantLeft when updating quantities.

Now, for some action:

 
$("h2").fadeOut('fast'); 
 
if (notInCart) { 
    prevCartItem = $('<li />', { 
        text : $('p:first', item).text(), 
        data : { id : id } 
    }).prepend($('<span />', { 
        'class' : 'quantity', 
        text : '0' 
    })).prepend($('<span />', { 
        'class' : 'price', 
        text : price 
    })).appendTo(cartList); 
}

First, we'll fade out the h2 prompt. Then, if the item isn't in the cart, we'll add it to the cart. To do this, we'll create a list item; then, we can pass an object literal as the second parameter to set properties of our new list item. We'll set the text to the product name; that comes from the first paragraph in the product item. Then, we set the data we talked about above.

Next, we'll prepend a span to this list item; we'll give it a class of 'quantity' (don't forget to put class in quotes, since it's a reserved word) and set the text to zero; yes, I know it should be one, since they've just put the item in the cart, but we'll increment that later . . . and you'll see why.

We'll prepend another span to the list item, this time for the price. We're going to float the price to the right, so it would seem logical to append it; but that would cause a float bug in IE; the span would actaully float right and below the list item.

Finally, we'll append the list item to the shopping cart list.

The last piece of this function will run whether or not the item is already in the cart or not:

 
quantLeftEl = $('p:last span', item); 
quantLeft   = parseInt(quantLeftEl.text(), 10) - 1; 
quantLeftEl.text(quantLeft); 
quantBoughtEl = $('.quantity', prevCartItem); 
quantBoughtEl.text(parseInt(quantBoughtEl.text(), 10) + 1); 
 
if (quantLeft === 0) { 
    item.fadeOut('fast').remove(); 
} 
 
total.text((parseFloat(total.text(), 10) + parseFloat(price.split('$')[1])).toFixed(2)); 
 
evt.stopPropagation(); 
return false;

First we'll get the quantity from the product item; this is the quantity left when the customer dragged the item to the cart; we'll then get the quantity that's really left; but that's a string, so we use the native function parseInt to convert it to a number (use 10 as the radix to assure that we get a decimal number) and subtract one from it. Then we reset the quantity, using jQuery's text method.

Next, we get the quantity the user has bought; this is the element with a class of 'quantity'; we use the prevCartItem as the context; this works because if the item was already in the cart, prevCartItem was set in that anonymous function; if it wasn't in the cart, we set it when we created the cart entry. We can then set the text value by getting the current value, converting to to a number, and adding one to it.

What happens when the quantity left hits zero? If it's zero, we'll fade the item out and remove it.

Finally, we have to update the total price. We've got the total span, so we can just reset the text; what we're doing is getting the current text, converting it to a number (this time we're using parseFloat to keep the cents), splitting the dollar sign off the price and converting that to a number and adding to two values. Finally, we'll use toFixed to make sure we're always showing the correct cents value.

Lastly, we don't want the drop drop event to bubble, so we'll stop its propagation and return false;


Step 4. The Completed Project

Good job, we're finished; here's a shot of our cart in action:


If you want to check out what the other drag and drop events do, add this to the script:

 
$('#cart').bind('dragleave', function (evt) { 
	console.log('dragleave'); 
}); 
 
$('.item') 
	.bind('dragend', function (evt) { 
		console.log('dragend'); 
	}) 
	.bind('dragstart', function (evt) { 
		console.log('dragstart'); 
	}) 
	.bind('drag', function (evt) { 
		console.log('drag'); 
	});

While native HTML5 drag and drop may not be completely ready for prime time just yet (Opera doesn't support it), it's definitely exciting to see where things are going!

Related Posts
  • Web Design
    HTML/CSS
    Build a Top Bar Off-Canvas Navigation With Foundation 5Foundation menu
    Today, we are going to combine ZURB's Foundation 5 Off-Canvas feature with our top bar navigation. The result will be a nice custom navigation for desktop users and a sleek off -canvas menu for tablet and mobile users. Read More…
  • Code
    HTML5
    HTML5: Battery Status APIPdl54 preview image@2x
    The number of people browsing the web using mobile devices grows every day. It's therefore important to optimize websites and web applications to accommodate mobile visitors. The W3C (World Wide Web Consortium) is well aware of this trend and has introduced a number of APIs that help with this challenge. In this article, I will introduce you to one of these APIs, the Battery Status API.Read More…
  • Web Design
    Case Studies
    How They Did It: Typekit's New HomepageTypekit retina
    Typekit recently redesigned their homepage with some new services in mind. When Typekit joined Adobe, they set out to bring us a new way to handle fonts on the web. Not only did they create a fairly simple way to embed fonts on the web, but they have now officially launched a desktop sync option, which allows Creative Cloud subscribers to sync fonts to their computer directly from Typekit. This has been in a beta form for a while now, and provides a much easier route to local fonts than finding them elsewhere!Read More…
  • Web Design
    UX
    Walk Users Through Your Website With Bootstrap TourTour retina
    When you have a web application which requires some getting used to from your users, a walkthrough of the interface is in order. Creating a walkthrough directly on top of the interface makes things very clear, so that's what we're going to build, using Bootstrap Tour.Read More…
  • Code
    JavaScript & AJAX
    Working With IndexedDB - Part 3Indexeddb retina preview
    Welcome to the final part of my IndexedDB series. When I began this series my intent was to explain a technology that is not always the most... friendly one to work with. In fact, when I first tried working with IndexedDB, last year, my initial reaction was somewhat negative ("Somewhat negative" much like the Universe is "somewhat old."). It's been a long journey, but I finally feel somewhat comfortable working with IndexedDB and I respect what it allows. It is still a technology that can't be used everywhere (it sadly missed being added to iOS7), but I truly believe it is a technology folks can learn and make use of today. In this final article, we're going to demonstrate some additional concepts that build upon the "full" demo we built in the last article. To be clear, you must be caught up on the series or this entry will be difficult to follow, so you may also want to check out part one.Read More…
  • Web Design
    HTML/CSS
    How to Customize the Foundation 4 Top BarFoundation preview
    Zurb's Foundation 4 features a brilliant top bar which has become almost symbolic of a Foundation site build. Today we're going to look at how you can implement it in a different way, placing it elsewhere on the page, giving you a custom and responsive horizontal navigation menu.Read More…