Switch On The Code RSS Button - Click to Subscribe
Oct
4

Javascript Tutorial - Drag & Drop Lists

Today we are going to take a look at another little nugget that many javascript libraries have these days - a drag and drop reorderable list. As usual, what we will be presenting is not a packaged solution that you should just pick up and drop on your own site (although you probably can) - it is more of an exploration on how you would go about implementing your own drag & drop list.


The example that we will be creating allows the user to pick up and drag any of the blocks in the list. The block picked up becomes partially transparent, and a light gray insertion marker (the size of the object picked up) appears in the spot the picked up block would be placed if the user dropped it. This insertion marker will move as the user moves the picked up item, always showing what will occur when the user drops the item.



Have fun with that? I thought so. So onto the code!

The first thing to note (and this is probably not surprising) is that we need code for dragging objects. Well, guess what - we already have that code, from our Draggable Elements tutorial. So if you have not read that tutorial , I suggest you do so - because that dragging code actually comprises most of the code we need for this drag and drop list.

Ok, lets take a look at the html behind this example list:

<div id="list"
    style="position:relative;border:solid 1px black;width:150px;">

  <div id="e1" style="height:50px;background-color:Green;"
      class="list">
</div>
  <div id="e2" style="height:140px;background-color:Blue;"
      class="list">
</div>
  <div id="e3" style="height:100px;background-color:Red;"
      class="list">
</div>
  <div id="e4" style="height:30px;background-color:Yellow;"
      class="list">
</div>
  <div id="e5" style="height:70px;background-color:Fuchsia"
      class="list">
</div>
</div>

So we have a surrounding div, and each of the list elements is another div inside the surrounding div. The heights are all set differently to give a more interesting list (and for testing to make sure the list works with arbitrarily sized elements). And it looks like all the list items have their style class set as list. I guess that would be a good reason to look at the style sheet for the drag and drop list:

.drag, .list
{
  width: 150px;
}

.drag
{
  z-index: 100;
  position:absolute;
  opacity: .50;
  filter: alpha(opacity=50);
}

.list
{
  position:relative;
  z-index: 1;
  opacity: 1;
  filter: alpha(opacity=100);
  top: 0px;
  left: 0px;
}

Ok, so we have two classes in this style sheet. One, list is for items in the list. The second, drag is for the item being dragged. There are a couple subtle things to note here (and some obvious stuff). First, the fact that list uses relative position and drag uses absolute position is critical. Using relative position for the list items lets us not care about the exact position of each item - we just stack them on. But using absolute position for the item we are dragging lets the z-index property actually work (so that the item being dragged will always be on top of everything else), and it also makes our life much easier when we need to figure out the exact pixel coordinates of the item being dragged.

The other interesting thing here is setting the opacity. A couple of browsers (notably IE 6 and 7) don't support the opacity style tag yet - it is part of the CSS 3 spec. So while setting opacity works for Firefox, Opera, and Safari, for IE we have to set a filter style tag, as you can see. Fortunately, whichever property the browser doesn't understand, it just ignores - so having both properties lets opacity work in all 4 major browsers.

That is it for html and css, lets move on into the javascript code:

var List;
var PlaceHolder;

function load()
{
  List = document.getElementById("list");
 
  PlaceHolder = document.createElement("DIV");
  PlaceHolder.className = "list";
  PlaceHolder.style.backgroundColor = "rgb(225,225,225)";
  PlaceHolder.SourceI = null;
 
  new dragObject("e1", null, null, null, itemDragBegin,
      itemMoved, itemDragEnd, false);
  new dragObject("e2", null, null, null, itemDragBegin,
      itemMoved, itemDragEnd, false);
  new dragObject("e3", null, null, null, itemDragBegin,
      itemMoved, itemDragEnd, false);
  new dragObject("e4", null, null, null, itemDragBegin,
      itemMoved, itemDragEnd, false);
  new dragObject("e5", null, null, null, itemDragBegin,
      itemMoved, itemDragEnd, false);
}

The drag and drop list code uses two global variables - List and PlaceHolder. List is set in the load function to the div surrounding all the list elements - mostly so we don't have to continuously call getElementById later on. PlaceHolder is actually created from scratch in the load function, and is the div we will use as a placeholder/insertion marker when an item is being dragged. We create the element, set the style class, give it a light gray background color, and initialize a property (that we will use extensively later on) to null.

The next thing we do in the load function is create drag objects for each of the elements in the list. We don't need to store these anywhere, we just need to create them and let them do their thing in the background. The important parts here are the functions that we pass in as the callbacks. For when a drag begins, we have the itemDragBegin function, for each move during a drag we have the itemMoved function, and for when the drag completes we have the itemDragEnd function.

First, lets attack the itemDragBegin function:

function itemDragBegin(eventObj, element)
{
  element.style.top = element.offsetTop + 'px';
  element.style.left = element.offsetLeft + 'px';
  element.className = "drag";
  PlaceHolder.style.height = element.style.height;
  List.insertBefore(PlaceHolder, element);
  PlaceHolder.SourceI = element;
}

This function is called when the user clicks on an item in the list and initiates a drag. As arguments to this function (courtesy of the drag object), we get the mouse down event object, and the element that was clicked on. First here we want to transition the element from being an item in the list to an item being dragged. We set the top and left positions to the offset top and left - which is so that the element stays in the same spot as we switch it from being relatively to absolutely positioned. The offsetTop and offsetLeft are useful properties - they hold the number of pixels between the top/left edge of the current element and the top/left edge of its parent container. Now that we have the position set, we set the style class to "drag".

Next we make the PlaceHolder the same height as the element being dragged - that way the insertion marker will be the correct size. Then we insert PlaceHolder into the list, right before the element being dragged. This ensures that the PlaceHolder element will appear in the exact spot that the element being dragged originally was in. Finally we set SourceI property on PlaceHolder to the element being dragged. We use this SourceI to keep track of the PlaceHolder is currently inserted before (since we will be moving the PlaceHolder around a lot as we need to change the location of the insertion marker).

So now the drag has begun. After this point, everytime the user moves the mouse, the drag element will be moved, and itemMoved will get called:

function itemMoved(newPos, element, eventObj)
{
  eventObj = eventObj ? eventObj : window.event;
  var yPos = newPos.Y
      + (eventObj.layerY ? eventObj.layerY : eventObj.offsetY);

  var temp;
  var bestItem = "end";
  for(var i=0; i<List.childNodes.length; i++)
  {
    if(List.childNodes[i].className == "list")
    {
      temp = parseInt(List.childNodes[i].style.height);
      if(temp/2>= yPos)
      {
        bestItem = List.childNodes[i];
        break;
      }     
      yPos -= temp;
    }
  }
 
  if(bestItem == PlaceHolder || bestItem == PlaceHolder.SourceI)
    return;
 
  PlaceHolder.SourceI = bestItem;
  if(bestItem != "end")
    List.insertBefore(PlaceHolder, List.childNodes[i]);
  else
    List.appendChild(PlaceHolder);
  }
}

I actually had to make a slight tweak to the dragging code here - where this callback gets called in the dragging code, it used to only pass the new position and the element - but I found that I needed the event object as well. So if you are using a copy of the dragging code from here, don't forget to add that in.

The first thing we do in this function is determine the exact y position (relative to the list container div) of the element being dragged. We already kind of know this, the top/left position of the element is stored inside of newPos, but most people don't expect the top edge of the element to be what triggers the movement of the insertion marker from one spot to another - they expect it to be relative to the cursor position. So we use the event object and grab the cursor position relative to the drag element, and add it to the top position of the drag element. For Firefox/Opera/Safari, this relative cursor position is stored inside the layerY variable on the event, and for IE it is stored in the offsetY variable.

Now that we have the position, we iterate through the items in the list. We get each item's height, and if the position of the the drag element is less than half that height, we have found our insertion point, and we break out of the loop. Otherwise, we subtract that height from the position variable. This means that if your mouse is in the top half of an element on the list (or in the bottom half of the element above it), we break out of the loop. If we complete the loop without finding anything, it means that the insertion marker should be at the bottom of the list.

If the list item we found is the PlaceHolder, we already have the insertion marker exactly where it needs to be, so we return without doing anything. This is also the case if the item we found is equal to the item we currently have in the SourceI variable on PlaceHolder. Otherwise, if we found an item, insert the PlaceHolder before that item. The act of inserting the PlaceHolder will automatically remove it from its previous poition (if it was in one), because an element can only be in one place at on time. If we found no item, we just append PlaceHolder at the end of the list.

And that takes care of moving the insertion marker as the user moves around the drag element. Now on to what happens when the user releases the mouse button and drops the element:

function itemDragEnd(element)
{
  if(PlaceHolder.SourceI != null)
  {
    PlaceHolder.SourceI = null
    List.replaceChild(element, PlaceHolder);
  }

  element.className = 'list';
  element.style.top = '0px';
  element.style.left = '0px';
}

This is actually really simple. If PlaceHolder has ever been placed in the list as an insertion marker, SourceI will not be null, so we reset SourceI back to null, and replace PlaceHolder in the list with the element that was just dropped. This means that the element being dropped gets added to the list exactly where the PlaceHolder was, and the PlaceHolder is removed from the list.

The only other thing to do is to clear the position values and set the element that was dropped back to the list style class.

And there you go! A drag and drop list in javascript. You can download the code here. Of course, this is only one possible implementation of a drag and drop list - perhaps you don't want the insertion marker to be as big as the item being dragged. Perhaps you want some animation for the movement of the blocks. Perhaps you would like other things that I have never even thought of. In which case, I encourage you to build off the concepts here and create some crazy drag and drop lists. And as always, questions and comments are welcome.



Posted in Javascript, All Tutorials by The Tallest |

7 Responses

  1. bhargav Says:

    Hi,
    This example really helps me a lot for my requirement.
    thanks a lot.

    can you tell me how to place items in multiple columns with the same drag & drop effect.

    i tried for it, but its not working. please take it as hi-priority.

  2. Munted Says:

    “Please take it as hi-priority”
    Thanks for that, cheered up my day :D

  3. Gary R. Says:

    When it says -

    “First here we want to transition the element from being an item in the list to an item being dragged. We set the top and left positions to the offset top and left - which is so that the element stays in the same spot as we switch it from being relatively to absolutely positioned.”

    How is it actually switched from relative to absolute positioning? Is it simply done by setting the top and left style positions?

  4. Kyle Says:

    I tried this. I read the previous “draggable elements” and that worked. I moved on to this one, followed everything step by step and it doesn’t work.

  5. The Tallest Says:

    A common problem is if the load function is called before the html. If you post your error, I might be able to help.

  6. Levi Says:

    I would like to do this exact same thing…the only problem is that i actually need to be able to keep the order that you move it in because i need to be able to order project elements from most important to least important for clients. I am using php. Any help would be appreciated.

  7. Achyutanand Nayak Says:

    Sub: How to do Drag and Drop action in listbox through javaScript

    Q:

    Hi friend

    I am having single listbox its contain some data like(one,two,three,four), i want
    to drag and drop action in listbox.

    Example:-
    first time it’s looks like this

    one
    two
    three
    four

    if i drag one to four (after drag and drop action) it’ll display

    two
    three
    four
    one

    now if i drag three to four (after drag and drop action) it’ll display

    two
    four
    three
    one

    Please help me it’s urgent and send me code also in my email ID (achyutanand@gmail.com).

Leave a Comment

Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.

Powered by WP Hashcash