agile-ajax

Multiple Column Sorting with Drag and Drop using Scriptaculous

The other day I wanted to do drag and drop between multiple columns using scriptaculous. Allowing this behavior is extremely simple, but out of the box the interaction feels clunky. Here, we'll be going through an example of how to do multi-column drag and drop with scriptaculous.

Enabling multi-column drag and drop just involves setting a single option, but without setting a few other options the dragging will feel jittery and won't allow us to drop on empty areas. Also, interaction with the server will require a small bit of consideration to support persistence of any changes.

For this example, we'll be using divs for the items and areas but the javascript will be the same if we used lists or any other HTML element to define your items and targets (we'll, of course, need to make sure that the correct tag option is specified in Sortable.create).

On the page, we'll have three areas styled to float next to each other (extra details left out for clarity).

    <div class="drop_target">
      <div>item one</div>
      <div>item two</div>
    </div>
    <div class="drop_target">
      <div>item three</div>
      <div>item four</div>
    </div>
    <div class="drop_target">
      <div>item five</div>
      <div>item six</div>
    </div>

Please note that in order for Sortable to work correctly the drop targets will need an ID, and each item element will also need a properly formatted ID for Sortable.serialize to work correctly. If we so desired, we could put in a few extra lines of code in the following javascript examples which will automatically add a unique ID to each drop target and item which doesn't already have one.

Now, to enable the basic drag and drop between the columns we just need to create a Sortable for each drop target. This is all we need for dragging to work between columns, but it won't be as slick as we want.

document.observe('dom:loaded', function() {
  targets = $$('.drop_target');
  targets.each(function(target) {
    Sortable.create(target.id, {tag:'div', containment: targets});
  });
});

The magic option here is containment. This specifies which elements on the page are valid drop targets when dragging an item. Since this requires that all the elements are already available when creating the Sortable, we used an event listener that fires when the DOM is finished loading in the browser. We're also using CSS selectors to grab the drop targets to make it easier to add/remove drop targets later down the line.

There are just two more options to specify. The constraint option removes a horizontal/vertical dragging constraint which makes drag and drop not smooth when using different drop targets, and dropOnEmpty will enable dropping items onto an empty drop target.

Sortable.create(target.id, {
  tag: 'div',
  containment: targets,
  constraint: false,
  dropOnEmpty: true
});

We can now drag and drop freely between the different drop targets, and lay them out any way we want. When we drag from one container to another, onUpdate will be called for both drop targets (since they both changed).

Since we are hitting a server to update order, we'll need to do one of two things. Sortable.serialize will include the HTML ID of the drop target in the parameters passed to the server as the key for the key-value pair. We could then use this on the server to determine which drop target is being updated (and update it appropriately). However, I don't like this because it isn't explicit and you end up parsing the request parameters looking for a certain pattern (plus, the coupling feels wrong).
Instead, we'll pass a known id for the drop target being updated as an explicit parameter. In straight javascript, it would look something like this (assume that known_drop_target_id is set to the appropriate value somewhere) :

Sortable.create(target.id, {
  tag: 'div',
  containment: targets,
  constraint: false,
  dropOnEmpty: true,
  onUpdate: function() {
    new Ajax.Request('http://foo.bar/baz', {
      parameters: Sortable.serialize(target.id) + '&drop_target=' + known_drop_target_id
    })
  }
});

In rails, you can use/generate a known value which your controller will look for (details left out for clarity):

<%= sortable_element_js(drop_target.id, :url => order_path(:id => drop_target) %>

And then in the controller (again, simplified for clarity):

@drop_target = DropTarget.find params[:id]
params[dom_id(@drop_target)].each_with_index do |item, index|
  // make sure the item is "in" the drop target in the correct order
end

In the end, drag and drop between multiple columns is only trivially more complex than a single column but provides a very cool and intuitive (in certain circumstances) interaction.

Comments: 1 so far

  1. I have stumbled with a similar situation. I have multiple sortables on the same page. Unfortunately, I want to hit the server only once because all the elements belong to the same set. I have been trying to figure this out till I found ur blog. Can you help in identifying the drop target?
    Thanks in advance for your help.

    Comment by Amit, Tuesday, August 26, 2008 @ 2:22 pm

Leave a comment

Powered by WP Hashcash

Who is Pathfinder?

Topics

Search

WordPress

Comments about this site: info@pathf.com