Once you know how to successfully drag one HTML element and drop it on another one, there is a new UX challenge: multiple elements that can be dragged and then dropped on any one of multiple droppables.
In Parts I, II and III of this series, we covered the critical concepts needed to implement HTML drag and drop. There, we talked about the dragstart event, the dataTransfer object, originalEvent object, drop event, dragenter event, dragleave event, and setDragImage method. These concepts got us to the point where we could drag one HTML element, and then drop it on another. All of this, with some UX considerations such as setting a custom drag image, updating the droppable element when something is dragged over it or when something is dropped on it.
When I was taking a deep dive into HTML5 drag-and-drop, I wanted to be able to wire-up a scenario where there were multiple “draggable” elements, and multiple “droppable” elements. So from a UX standpoint, my goals were:
- Any draggable could be dropped on any droppable
- Each droppable would change appearance when an element was dragged over it
- Each droppable would change appearance when an element was dropped onto it
- Each droppable would accept only one dropped element at a time
- The container that originally held the droppables would change appearance when empty
- The container that originally holds the droppables could accept any one or all of the droppables if they are dragged back to it
When I first attempted this, I got to a point where this all seemed like a lot of moving parts. Fortunately, a substantial portion of the challenge has been solved in parts I, II and III of this series, and I hope that if you have already read those articles, you have a solid understanding of how to do the following:
- Make an element draggable
- Set a custom drag element
- Make an element droppable
- Update the droppable when an element is dragged over it
- Update the droppable when an element was dropped onto it
So with that knowledge, our task list shrinks a bit:
- Any draggable can be dropped on any droppable
- Each droppable will accept only one dropped element at a time
- The container that originally held the droppables will change appearance when empty
- The container that originally held the droppables will accept any one or all of the droppables if they are dragged back to it
From here on, I will walk through the steps I took to accomplish these goals. There are probably other ways to go about solving these problems, but I am sharing the approach I took.
Task # 1: Any draggable can be dropped on any droppable
Well this was the easiest step. In parts I, II and III of this series, all of the JavaScript event binding was accomplished using classes instead of IDs. Because of this, adding more draggables and droppables in the HTML solved this problem. The existing JavaScript worked fine.
Task # 2: Each droppable will accept only one dropped element at a time
There are two things I needed to do in order to solve this problem: 1) create two separate “drop” event handlers, one for the multiple droppable HTML elements and another for the container that originally held the droppables; 2) in “drop” event handlers of the multiple droppables, check to see if the droppable element already has a child element.
Before I discuss the fix for task # 2, take a look at this JSFiddle link: http://jsfiddle.net/v9hawcof/5/
That link demonstrates the behavior that I do NOT want: more than one draggable can be dropped onto a droppable.
Base HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<!DOCTYPE HTML> <html> <head> </head> <body> <article> <section id="content"> <p class="instructions">Drag any blue square to any light brown box, or back to its original container. Each light brown box in the top row will accept only one blue square at a time. If you want to view the JavaScript for this example, click <a href="js/part-iv.js" target="_blank">here</a>.</p> <div class="drop-elements"> <div class="dropElement droppable"></div> <div class="dropElement droppable"></div> <div class="dropElement droppable"></div> <div class="dropElement droppable"></div> </div> <div class="drag-elements droppable multipleChildren hasChild"> <div id="drag1" class="dragElement redBorder">Drag me!</div> <div id="drag2" class="dragElement greenBorder">Drag me!</div> <div id="drag3" class="dragElement orangeBorder">Drag me!</div> <div id="drag4" class="dragElement pinkBorder">Drag me!</div> </div> <section> </article> <script src="js/jquery-1.11.1.min.js"></script> <script src="js/part-iv.js"></script> </body> </html |
So in the code example above, we have the base HTML for the rest of the examples in this article. For brevity’s sake, I’ve stripped out all but the most essential areas of the markup so that you can focus on the technologies demonstrated in this article. When you view the page source for the final working example, you will see that there is more HTML, but it is for presentational purposes only.
Example # 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//each of the four light-brown boxes at top have this bound to their drop event var dropHandlerSingle = function (event) { var id = ''; //prevent the browser from any default behavior event.preventDefault(); //only allow one child element at a time if($(this).children().length){return;} //get a reference to the element that is being dropped id = event.originalEvent.dataTransfer.getData("id"); //add the hasChild class so that the UI can update $(event.target).addClass('hasChild'); //trigger the custom event so that we can update the UI $(document).trigger('custom:dropEvent'); //move the dragged element into the drop target event.target.appendChild(document.getElementById(id)); }; |
Here is the JSfiddle link for Example # 1: http://jsfiddle.net/v9hawcof/6/
Now in Example # 1, I’ve added a new function called: “dropHandlerSingle”. This event handler is dedicated to the four light brown boxes at top (i.e. the “multiple droppables”). In the function I check to see if the droppable element has any children. If so, then I exit immediately. You may also notice that I trigger this custom event: “custom:dropEvent”. That will be explained shortly.
Task # 3: The container that originally held the droppables will change appearance when empty
Example # 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//after something is dropped $(document).on('custom:dropEvent',function(){ //make sure the DOM has been updated setTimeout(function(){ //check each droppable element to see if it has a child $('.droppable').each(function(){ //if this element has no children if (!$(this).children().length){ //remove the hasChild class $(this).removeClass('hasChild'); } }); },50); }); |
Here is the JSfiddle link for Example # 2: http://jsfiddle.net/v9hawcof/7/
In Example # 2, you see the event handler for “custom:dropEvent”. The reason I used the setTimeout method, is because of timing challenges. When the drop event occurs, the DOM is not in the state needed in order to query it and figure out what is going on (i.e. which droppable elements have children). This event handler for “custom:dropEvent” will check each droppable element, and if it has no children, it removes the CSS class that gives it a “hasChildren” appearance (i.e. the background color changes from dark brown to light brown).
Task # 4: The container that originally held the droppables will change appearance when empty
Example # 3A
1 2 |
//bind the dropHandlerMultiple function to the .droppable.multipleChildren element $('.droppable.multipleChildren').bind('drop',dropHandlerMultiple); |
Example # 3B
1 2 3 4 5 6 7 8 9 10 11 12 |
//the box that holds the four blue dragable boxes on page load has this bound to its drop event dropHandlerMultiple = function (event) { event.preventDefault(); var id = event.originalEvent.dataTransfer.getData("id"); $(event.target).addClass('hasChild'); event.target.appendChild(document.getElementById(id)); $(document).trigger('custom:dropEvent'); }; |
Example # 3C
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//bind the appropriate handlers for the dragover, dragenter and dragleave events $('.droppable').bind({ dragover: allowDragover, dragenter: function() { //ignore this event for the original container of the drag elements if ( $(this).hasClass('multipleChildren') ){return;} $(this).addClass('dragEnter'); }, dragleave: function() { $(this).removeClass('dragEnter'); } }); |
Here is the JSfiddle link for Example # 3: http://jsfiddle.net/v9hawcof/8/
In Example # 3A, I bind an event handler for the container that originally held the droppables. In Example # 3B you’ll see that event handler: the “dropHandlerMultiple” function. The “dropHandlerMultiple” event handler does pretty much the same thing as the “dropHandlerSingle” event handler. The only difference is that the “dropHandlerMultiple” function does not check to see if it has children already. This is the critical step in accomplishing task # 4. In Example # 3B, you’ll see the code that tells the dragenter event handler to exit immediately if an element is dragged over the container that originally held the droppables. This is not a critical feature. I suppose I could have allowed the dragenter UX to apply to that container as well.
Here is the working example for this article: http://examples.kevinchisholm.com/html5/drag-and-drop/part-iv.html
Here is the JavaScript for this article’s working example: http://examples.kevinchisholm.com/html5/drag-and-drop/js/part-iv.js
Summary
As you can see, everything in this article is really a matter of personal choice. What I’ve done is detail how I decided to solve a particular UX challenge. Now how you approach this same set of tasks is completely up to you. I found that while attempting all of this, I learned a few things about HTML5 drag and drop. I hope you will too.