Dragging-and-dropping between accordions
Some applications require a more fluid layout than others, not just from a screen resolution perspective, but from a functional one too. The accordion widget is a static grouping component that is used to organize smaller components into sections. We can hide all the irrelevant material simply by expanding the section we're interested in. As we have seen in the Sorting accordion sections recipe, we can provide an accordion whose structure can be manipulated by the user. Indeed, this has become the expectation of the users en masse—UI configuration by drag-and-drop.
The sortable accordion focuses on a single accordion. In the spirit of giving users freedom within the confines of the application of course, why don't we see if we can support moving an accordion section to a new accordion?
Getting ready
For this experiment, we'll need two basic accordions. The markup should assume a form along the lines of the following:
<div id="target-accordion" style="width: 30%"> <h3>Section 1</h3> <div> <p>Section 1 content</p> </div> <h3>Section 2</h3> <div> <p>Section 2 content</p> </div> <h3>Section 3</h3> <div> <p>Section 3 content</p> </div> </div> <p></p> <div id="accept-accordion" style="width: 30%"> <h3>Section 4</h3> <div> <p>Section 4 content</p> </div> <h3>Section 5</h3> <div> <p>Section 5 content</p> </div> <h3>Section 6</h3> <div> <p>Section 6 content</p> </div> </div>
How to do it...
With that in place, let's turn this markup into two accordions. We'll first extend the accordion widget with some fancy drag-and-drop behavior. The intent is to allow the user to drag accordion sections from the first widget to the second. Here is how it's done:
(function( $, undefined ) { $.widget( "ui.accordion", $.ui.accordion, { options: { target: false, accept: false, header: "> h3, > div > h3" }, _teardownEvents: function( event ) { var self = this, events = {}; if ( !event ) { return; } $.each( event.split(" "), function( index, eventName ) { self._off( self.headers, eventName ); }); }, _createTarget: function() { var self = this, draggableOptions = { handle: "h3", helper: "clone", connectToSortable: this.options.target, }; this.headers.each( function() { $( this ).next() .addBack() .wrapAll( "<div/>" ) .parent() .draggable( draggableOptions ); }); }, _createAccept: function() { var self = this, options = self.options, target = $( options.accept ).data( "uiAccordion" ); var sortableOptions = { stop: function ( event, ui ) { var dropped = $(ui.item), droppedHeader = dropped.find("> h3"), droppedClass = "ui-draggable", droppedId; if ( !dropped.hasClass( droppedClass ) ) { return; } // Get the original section ID, reset the cloned ID. droppedId = droppedHeader.attr( "id" ); droppedHeader.attr( "id", "" ); // Include dropped item in headers self.headers = self.element.find( options.header ) // Remove old event handlers self._off( self.headers, "keydown" ); self._off( self.headers.next(), "keydown" ); self._teardownEvents( options.event ); // Setup new event handlers, including dropped item. self._hoverable( droppedHeader ); self._focusable( droppedHeader ); self._on( self.headers, { keydown: "_keydown" } ); self._on( self.headers.next(), { keydown: "_panelKeyDown" } ); self._setupEvents( options.event );
// Perform cleanup $( "#" + droppedId ).parent().fadeOut( "slow", function() { $( this ).remove(); target.refresh(); }); dropped.removeClass( droppedClass ); } }; this.headers.each( function() { $(this).next() .addBack() .wrapAll( "<div/>" ); }); this.element.sortable( sortableOptions ); }, _create: function() { this._super( "_create" ); if ( this.options.target ) { this._createTarget(); } if ( this.options.accept ) { this._createAccept(); } }, _destroy: function() { this._super( "_destroy" ); if ( this.options.target || this.options.accept ) { this.headers.each( function() { $( this ).next() .addBack() .unwrap( "<div/>" ); }); } } }); })( jQuery ); $(function() { $( "#target-accordion" ).accordion({ target: "#accept-accordion" }); $( "#accept-accordion" ).accordion({ accept: "#target-accordion" }); });
We now have two basic-looking accordion widgets. However, if the user is so inclined, they can drag a section of the first accordion into the second.
How it works...
This might seem like a lot of code at the first glance, but for relatively little (approximately 130 lines), we're able to drag accordion sections out of one accordion and into another. Let's break this down further.
We're adding two accordion options with this widget extension: target
and accept
. Target allows us to specify the destination of sections of this accordion. In the example, we used the second accordion as the target for the first accordion, meaning that we can drag from target-accordion
and drop into accept-accordion
. But, in order to make that happen, the second accordion needs to be told where to accept sections from; in this case, it is target-accordion
. We're essentially using these two options to establish a drag-and-drop contract between the two widgets.
This example uses two interaction widgets: draggable and sortable. target-accordion
uses draggable. If the target
option was specified, the _createTarget()
method gets called. The _createTarget()
method goes through the accordion sections, wraps them in a div
element, and creates a draggable()
widget. This is how we're able to drag sections out of the first accordion.
If the accept
option was specified, the _createAccept()
method gets called. This follows the same pattern of wrapping each accordion header with its content in a div
element. Except here, we're making the entire accordion widget sortable()
.
This may seem counterintuitive. Why would we make the second accordion that wants to accept new sections into sortable? Would it not make more sense to use droppable? We could go down that route, but it would involve a lot of work where we're utilizing the connectToSortable
option instead. This is a draggable
option specified in _createTarget()
where we say that we would like to drop these draggable items into a sortable widget. In this example, sortable is the second accordion.
This solves the problem of deciding on where exactly to drop the accordion section relative to other sections (the sortable widget knows how to handle that). However, an interesting constraint with this approach is that we must clone the dragged item. That is, the section that ultimately gets dropped into the new accordion is just a clone, not the original. So we must deal with that at drop time.
As part of the sortable options defined in _createAccept()
, we provide a stop
callback. This callback function is fired when we've dropped a new accordion section into the accordion. Actually, this gets fired for any sorting activity, including new sections being dropped. So, we must take care to check what we're actually working with. We do so by checking whether the item has a draggable
class attached to it, and if so, we can assume we're dealing with a new accordion section.
Keep in mind that this newly dropped accordion section is simply a clone of the original, so some interesting things need to happen before we can start inserting it into the accordion. First, this new section has the same ID as the original. Eventually, we're going to remove the original from the first accordion, so we store that ID for later use. Once we have it, we can get rid of the dropped section's ID so as to avoid duplicates.
With that taken care of, we have the new DOM element in place, but the accordion widget knows nothing about it. This is where we reload the headers, including the newly-dropped header. The new accordion section still isn't functional because it doesn't handle events properly, so expanding the new section will not work, for example. To avoid strange behavior, we turn off all event handlers and rebind them. This puts the new accordion in its new context while the events are turned on.
We now have a new section in accept-accordion
. But we can't forget about the original section. It still needs to be removed. Recall that we stored the original section's DOM ID, and we can now safely remove that section and refresh the accordion to adjust the height.