Tuesday, April 9, 2013

Mixing jQuery UI Draggable, Droppable, and Sortable Interaction Widgets

jqueryui-drag-drop-sort

The jQuery UI library provides quite a few useful mouse interaction widgets which make building solutions that need drag/drop functionality significantly easier. Most of them have the ability to integrate cleanly with each other which reduces the amount of "glue" code required to make these interaction work together. I've been working with the Sortable widget for some time now to enable my users to rearrange items in a list. One of the challenges I faced was how to design a consistent method of adding/removing items to/from the list via drag/drop interactions and provide meaningful feedback to the user. There are several pieces to making all the interactions work together harmoniously. The setup I'm presenting here takes into account not only the required functionality but also creating consistent styles and visual cues to guide the user's interaction. There are probably other ways to handle it but this is the solution I've found that provides the most flexibility for my intended uses.

My basic approach is to have item instances available in a draggable widget. These can be move into a droppable widget or a sortable widget. Items from the sortable widget can be moved around as usual within the widget but also dragged out onto the droppable widget. When in sorting mode, I want the item to be constrained to the list, however, that would make it impossible to move the item out of the list to the droppable area. My solution was to create an area around the sortable item that is a separate drag area that would enable removing the item from the sortable area. This may not be necessary if you don't care about constraining the item to the parent list. The item could just be dragged to the droppable area. However, I feel like having the two distinct modes of operation provides more flexibility in the design and an opportunity to provide better feedback to the user about the activity they are performing. The biggest advantage to using a separate draggable is that you can configure it completely different than the sortable's drag configuration. Because of that flexibility, I went through the effort to find a way to combine the two together. The trick is to do it and not have one interfere with the operation of the other.

To achieve the solution outlined above, I started with the following basic item HTML:



<div class="item-wrapper">
<div class="drag-handle"></div>
<div class="item-container">Item 1</div>
</div>



Even though the .drag-handle is not a parent of the .item-container, it will be styled to appear that way:

jqueryui-drag-drop-sort-item-html

Technically, this arrangement is not necessary for differentiating the draggable/sortable behaviors. That can easily be achieved with the cancel option in the draggable configuration. However, if you want to toggle class styles when hovering over the different elements, you'll have the most flexibility with the two elements being siblings instead of parent/child. Both the jQuery.hover() and hoverIntent use mouseenter and mouseleave events to trigger their handlers. These two events differ from the behavior of mouseover/out. Read the discussion in the jQuery Docs for more information. Given that I might use either hovering method to create cues for the user, using the sibling arrangement of the drag handle and item container works best.

The .item-wrapper will be the actual target for all drag operations. The children will be configured to act as handles to initiate the actual dragging. The styling is important to properly position the .drag-handle and .item-container:



.item-wrapper {
position: relative;
padding: 3px;
}

.drag-handle {
position: absolute;
border: 3px solid transparent;
left: 0;
right: 0;
top: 0;
bottom: 0;
}

.item-container {
position: relative;
text-align: center;
}



The .item-wrapper has a 3px padding that matches the 3px border of the .drag-handle to create an area around the .item-container that will allow activating the draggable instead of the sortable operation. It will also have hovering events attached appropriately to enable styling visual cues for the user.

The next step is to create containers for each widget where the items can be held and moved around:



<div class="wrapper">

<h5>Draggable</h5>
<div class="drag-container ui-corner-all">
...
</div>

<h5>Droppable</h5>
<div class="drop-container ui-corner-all">
...
</div>

<h5>Sortable</h5>
<div class="sort-container ui-corner-all">
...
</div>

</div>



At any given time, Draggable and Droppable will only have one item and Sortable can have N items. At page load, I setup four items - 3 in the Sortable container (labeled Item 1, Item 2, and Item 3), one in the Draggable container (labeled Item 4), and one in the Droppable container (labeled Empty). When you drag Item 4 from the Draggable and drop it in a valid container (either the Droppable or the Sortable), the item number will increase (so the Sortable/Droppable will now have Item 4 and the Draggable will show Item 5). Playing with the demo might make more sense and help clarify the remaining discussion.

First, we'll configure the Draggable widget:



$('.drag-container')
.find('.item-wrapper').draggable({

cursor: 'move',
zIndex: 200,
opacity: 0.75,
scroll: false,
containment: 'window',
appendTo: document.body,
helper: 'clone',
connectToSortable: '.sort-container',
start: fixHelper

})




The most important part of this setup is the helper, connectToSortable, and start callback configuration items. The connectToSortable feature allows simple integration with a Sortable widget. You don't need to do anything other than specify that argument and ensure the helper is equal to "clone". I discuss the fixHelper() function later with the Sortable setup. I also skipped the hovering cues which I'll cover later as well.

The droppable is pretty straight forward and just needs to handle the drop event:



$('.drop-container').droppable({

hoverClass: 'mx-content-hover',
drop: function ( e, ui ) {

$(this).find('.item-container').html( ui.draggable.children('.item-container').html() );

if ( ui.draggable.parent().is('.drag-container') )
$('.drag-container .item-container').html('Item ' + (++items));
else
// Defer removing the item after exiting the current call stack
// so the Draggable widget can complete the drag processing
setTimeout(function () { ui.draggable.remove(); }, 0);

}
});



In this case, it copies the HTML from the source item into the droppable's item's HTML. Additionally, it has to determine which container was the source of the drag. If its the Draggable, then the item count needs to be updated. If its the Sortable, then we want to remove the item from the list.

The Sortable configuration is a little more complex since it not only configures the Sortable interaction but also has to attach Draggable widgets to each item in the Sortable container. It has to configure the items both at page initialization and each time a new item is dragged into the list:



$('.sort-container')
.sortable({

containment: 'parent',
handle: '.item-container',
tolerance: 'pointer',
helper: 'clone',
start: fixHelper,
update: /* see below */

}).find('.item-wrapper')
.draggable({
cursor: 'move',
zIndex: 200,
opacity: 0.75,
handle: '.drag-handle',
scroll: false,
containment: 'window',
appendTo: document.body,
helper: 'clone',
start: fixHelper
});



There are several things to note when looking at the configuration of the Sortable and the Draggables. First, both are targeting the same DOM elements - .item-container. Even though the Sortable is attached to the parent container, the drag operations are attached to its children (.item-container). Since this will cause issues if both the Sortable and Draggable try to respond to drag events on the same element, some of the other configuration options need to be used to restrict the scope of which part of the element each widget will respond to. This is achieved with the handle option in the Sortable and Draggable widgets:



$('.sort-container')
.sortable({
...
handle: '.item-container',
...
}).find('.item-wrapper')
.draggable({
...
handle: '.drag-handle',
...
});



Effectively, this limits the Sortable to responding to drag events on the .item-container element and Draggable will only listen to events on the .drag-handle. The final point of note on the setup is the helper configuration. It is set to "clone" and the start callback is used to normalize the cloned element across all the draggable widgets:



function fixHelper( e, ui ) {

var $ctr = $(this);

ui.helper
.addClass('mx-state-moving ui-corner-all')
.outerWidth($ctr.outerWidth())
.find('.mx-content-hover')
.removeClass('mx-content-hover')
.end();
}



This ensures the cloned element doesn't have any of the classes related to hover cues assigned, matches the width of the item as it appeared in the container, and adds a class to style the helper while moving:



.item-wrapper.mx-state-moving {
background-color: #fcefa1;
color: #000;
margin: 0;
}



The final problem to manage in the Sortable is handling a new item from the Draggable. The Sortable will automatically add an item from the Draggable container due to the connectToSortable option in the Draggable configuration. However, if you need to perform additional handling on the new item, you'll need to work with one of the callbacks to attach that functionality. This turned out to be a little more difficult than I expected. I was hoping to be able to detect that a drag operation was initiated by the Draggable (similar to how I did it in the Droppable) and also determine where the item was dropped in the Sortable. However, none of the callbacks provided all this information at the same time. In one case, the placeholder is available but the sender is not. The sender also seems to only work when the item is coming from another Sortable. To work around these issues, I decided not to include the .drag-handle element in the Draggable item. When its dropped on the Sortable, I can inspect the ui.item element in the Sortable update() callback to determine if its missing that element. If it is missing, then I know it came from the Draggable and I need to run the code to handle it. If it is not missing, then I know its a normal sort operation in the Sortable widget and no further processing is required:


$('.sort-container')
.sortable({

...

update: function ( e, ui ) {

/* Check if the drag handle is missing */
if ( ui.item.find('.drag-handle').length == 0 ) {

/* It is so increment the item counter */
$('.drag-container .item-container').html('Item ' + (++items));

/*
And setup the item so it has a drag handle and
responds to drag events
*/
ui.item
.find('.item-container')
.before( $('<div class="drag-handle">') )
.parent()
.draggable({

cursor: 'move',
zIndex: 200,
opacity: 0.75,
handle: '.drag-handle',
scroll: false,
containment: 'window',
appendTo: document.body,
helper: 'clone',
start: fixHelper

});

/*
Reset the containment. Somehow it breaks when adding
the external item
*/
$(this).sortable('option', 'containment', 'parent');
}

}

...

});


That takes care of the dragging functionality. Now we need to add some visual cues when the user hovers over various areas that are draggable. I simply toggle a class on any target I'd like to enable hovering. This handler can be used in either the jQuery hover() or the hoverIntent plugin:


function toggleHover( e ) {

if ( e.type == 'mouseenter' )
$(this).addClass( 'mx-content-hover' );
else
$(this).removeClass( 'mx-content-hover' );

}


Next, we need to define styles for each hover target:



.drag-container .item-wrapper.mx-content-hover {
background-color: #cce0ff;
color: #000;
}

.drop-container.mx-content-hover {
background-color: #ccff99;
}

.sort-container .drag-handle.mx-content-hover {
border-color: #cce0ff;
}

.sort-container .item-wrapper.mx-content-hover {
color: #000;
}



Finally, the jQuery hover handler (or hoverIntent depending on if you'd like a delayed response) needs to be setup on all the hover targets so their classes can be toggled:



$('.drag-container')
.find('.item-wrapper')
.draggable(...)
.hover( toggleHover );

$('.sort-container')
.sortable(...)
.find('.item-wrapper')
.draggable(...)
.hover( toggleHover )
.find('.drag-handle')
.hover( toggleHover );



That covers my basic approach to assembling several jQuery interaction widgets together and leveraging their built-in functionality while still being able to enhance the functionality where needed. While its certainly not a one-size-fits-all solution, I've found that it serves as a nice pattern that can be used as a starting point for more elaborate designs.