Dragging and dropping table rows in Javascript

Summary: This article tells you how to implement drag and drop for HTML tables in Javascript. You can download the source here and play with the demo here.

Updated: now copes with multiple tables on the same page, non-drag and non-drop rows (such as headers) and embedding form elements. Also see my jQuery plug-in which does much more.

There are many articles on implementing drag and drop in Javascript and many excellent frameworks and libraries that provide you with everything you need. In fact I use Script.aculo.us quite a bit. However, I haven’t found anything much that tells you how to re-order rows in a table. Table rows are different from other elements normally used for drag and drop such as list items or divs because they can’t be moved about in the same way. It wouldn’t make sense to have absolute positioning on table rows—they sort of have to be in the table. There are also limitations on the styles you can put on rows, you can’t have a border round them for example. So, the solution to this problem is slightly different from other drag and drop mechanisms. Here’s and example, try dragging the rows about:

1 One some text
2 Two some text
3 Three some text
4 Four some text
5 Five some text
6 Six some text

Drag and Drop basics

I won’t spend a great deal of time explaining the basics of drag and drop. Instead I’ll point you at How to Drag and Drop in Javascript. This article provides a good explanation including code that you can download and use.

The steps you need are:

  1. capture the mouse move event for the whole document so you can track where the mouse is.
  2. capture the onmouseup event for the whole document so we know when our dragged thing is dropped.
  3. add onmousedown functions for each draggable row so that we know which row is being dragged.

In order to make this neat and reusable I have encapsulated the required data in a class (we’re also going to add functionality in here later):

function TableDnD() {
    /** Keep hold of the current drag object if any */
    this.dragObject = null;
    /** The current mouse offset */
    this.mouseOffset = null;
    /** The current table */
    this.table = null;
    /** Remember the old value of Y so that we don't do too much processing */

    this.oldY = 0;

    // rest of the code goes here...
}

The class has instance variables for the currently dragged object, the current mouse offset, the current table, and the oldY so that we can detect if we’re moving up or down. We will create one TableDnD object for each table for which we want to enable to drag and drop

Setting up the event handlers

Now we need to capture the document.onmousemove and document.onmouseup events so that we can track where the user drags and drops the row. The first version that I developed captured the events insidethe TableDnD object, but this doesn’t work if you want multiple tables on the same page. So we have to abstract this out and have global handlers for the whole page. We also therefore need to know which TableDnD object we’re currently concerned with (in other words which one, if any, initiated the drag). So, here are the event handlers event handlers we need (and a global variable to keep track):

/** Keep hold of the current table being dragged */
var currenttable = null;

/** Capture the onmousemove so that we can see if a row from the current
 *  table if any is being dragged.
 * @param ev the event (for Firefox and Safari, otherwise we use window.event for IE)
 */
document.onmousemove = function(ev){
    if (currenttable && currenttable.dragObject) {
        ev   = ev || window.event;
        var mousePos = currenttable.mouseCoords(ev);
        var y = mousePos.y - currenttable.mouseOffset.y;
        if (y != currenttable.oldY) {
            // work out if we're going up or down...
            var movingDown = y > currenttable.oldY;
            // update the old value
            currenttable.oldY = y;
            // update the style to show we're dragging

            currenttable.dragObject.style.backgroundColor = "#eee";
            // If we're over a row then move the dragged row to there so that the user sees the
            // effect dynamically
            var currentRow = currenttable.findDropTargetRow(y);
            if (currentRow) {
                if (movingDown && currenttable.dragObject != currentRow) {
                    currenttable.dragObject.parentNode.insertBefore(currenttable.dragObject, currentRow.nextSibling);
                } else if (! movingDown && currenttable.dragObject != currentRow) {
                    currenttable.dragObject.parentNode.insertBefore(currenttable.dragObject, currentRow);
                }
            }
        }

        return false;
    }
}

// Similarly for the mouseup
document.onmouseup = function(ev){
    if (currenttable && currenttable.dragObject) {
        var droppedRow = currenttable.dragObject;
        // If we have a dragObject, then we need to release it,

        // The row will already have been moved to the right place so we just reset stuff
        droppedRow.style.backgroundColor = 'transparent';
        currenttable.dragObject   = null;
        // And then call the onDrop method in case anyone wants to do any post processing
        currenttable.onDrop(currenttable.table, droppedRow);
        currenttable = null; // let go of the table too
    }
}

In the onmousemove method itself, we first of all check to see if we have a current table, and if so, does that have a dragObject. If not, we don’t need to do anything. If we do have a dragObject, then we need to get the event. In Internet Explorer, the event is global and accessible using window.event, in Firefox and other browsers it is passed in as a parameter, so we need to check for both cases. Once we have that, we can get the mouse coordinates (again code to follow), and check the y position. Because we’re only dragging rows, we’re only interested in the vertical direction, if the y value hasn’t changed, then we don’t need to do anything (we could also put in a threshold here so we don’t worry about small movements).

Assuming that y has changed, we can work out whether it’s an upwards or downwards direction by comparing it with the old value (you’ll see why we need this in a moment). Then we can set the background colour of the dragObject to something to make it obvious it is being dragged (we’re fairly limited as to what styles we can apply to rows—a neater approach would be to add and subtract a class, like that the style could be controlled by a stylesheet rather than code and it would be inherited by the constituent cells). Next we find out which row the mouse is currently over (again we only really need to worry about the y coordinate, we’re not really worried if the mouse strays left or right outside the table—though we could change for that if needed).

Now we know which row the mouse is over, we want to move our row to be before or after the current row depending on whether we’re moving up or down. After a quick check to make sure that we’re not moving it to where it already is, we use parentNode.insertBefore(...) to move the row. If we’re moving down, we get the nextSibling and insert before that, otherwise we just insert before the current row.

If we do move the row, then we return false from the event handler so that no other related event fires and default handling isn’t engaged.

The onmouseup method is much more straight forward. All we need to do is reset the style and then forget the dragObject and the current table.

That’s what happens when we’re actually dragging something, but how do we initiate the drag? We need to capture the mouse down event on the rows that we want to drag. Back in our TableDnD class we add an init method which takes the table as a parameter and sets everything up:

    /** Initialise the drag and drop by capturing mouse move events */

    this.init = function(table) {
        this.table = table;
        var rows = table.tBodies[0].rows; //getElementsByTagName("tr")
        for (var i=0; i<rows.length; i++) {
            // John Tarr: added to ignore rows for which the NoDrag attribute is set
            var nodrag = rows[i].getAttribute("NoDrag")
            if (nodrag == null || nodrag == "undefined") { // There is no NoDrag attribute so make draggable
                this.makeDraggable(rows[i]);
            }
        }
    }

We get passed in the table whose rows we want to be able to drag and drop, so we remember that, then we go through all the rows in the table body and make them “draggable” (code for this coming soon). John Tarr contacted me to say that he needed to be able to control which rows were draggable and which not (for example headers shouldn’t be draggable). So he added a simple NoDrag attribute which can be used to switch off “draggability”.

Of course you might want to do something with the table once row has been dropped, so I’ve made the method call an onDrop method passing it the table and the dropped row. The default implementation does nothing, but you can redefine it to do whatever you need to (in fact in my current project I use this to make an Ajax call to let the server know the new order of the rows).

    /** This function is called when you drop a row, so redefine it in your code
        to do whatever you want, for example use Ajax to update the server */
    this.onDrop = function(table, droppedRow) {
        // Do nothing for now
    }

Getting the coordinates

Now we need some methods that get the mouse position from an event:

    /** Get the position of an element by going up the DOM tree and adding up all the offsets */
    this.getPosition = function(e){
        var left = 0;
        var top  = 0;

        while (e.offsetParent){
            left += e.offsetLeft;
            top  += e.offsetTop;
            e     = e.offsetParent;
        }

        left += e.offsetLeft;
        top  += e.offsetTop;

        return {x:left, y:top};
    }

    /** Get the mouse coordinates from the event (allowing for browser differences) */
    this.mouseCoords = function(ev) {
        if(ev.pageX || ev.pageY){
            return {x:ev.pageX, y:ev.pageY};
        }
        return {
            x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
            y:ev.clientY + document.body.scrollTop  - document.body.clientTop
        };
    }

    /** Given a target element and a mouse event, get the mouse offset from that element.
        To do this we need the element's position and the mouse position */

    this.getMouseOffset = function(target, ev){
        ev = ev || window.event; // In FireFox and Safari, we get passed the event, in IE we need to get it

        var docPos    = this.getPosition(target);
        var mousePos  = this.mouseCoords(ev);
        return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
    }

The first method, getPosition, takes an element and walks up the DOM using offsetParent to add up all the offsets to get the absolute position of the element. It returns the position as an object with x and y instance variables.

The next method, mouseCoords takes an event and extracts the coordinates from it. Firefox and other browsers use event.pageX and event.pageY to store the position, so we can just return this. Internet Explorer however uses event.clientX and event.clientY. What’s more is that these values are for the current window, not the position from the top of the page, so in order to be able to map these to the same values as pageX and pageY, we have to add in the current scroll position of the document.

The final method in this trio is getMouseOffset this takes a target element and an event and works out where the mouse is in relation to the element. It processes the event in the same way as we saw before to allow for browser differences and the calls the two methods above to get the positions and returns the relative position of the mouse.

Which rows?

We need two more methods to complete our class:

    /** Take an item and add an onmousedown method so that we can make it draggable */
    this.makeDraggable = function(item){
        if(!item) return;
        var self = this; // Keep the context of the TableDnd inside the function
        item.onmousedown = function(ev){
            // get the event source in a browser independent way
            var target = getEventSource(ev);
            // if it's an INPUT or a SELECT, then let the event bubble through, don't do a drag
            if (target.tagName == 'INPUT' || target.tagName == 'SELECT') return true;
            self.dragObject  = this;
            self.mouseOffset = self.getMouseOffset(this, ev);
            return false;
        }
        item.style.cursor = "move";
    }

    /** We're only worried about the y position really, because we can only move rows up and down */

    this.findDropTargetRow = function(y) {
        var rows = this.table.tBodies[0].rows;
        for (var i=0; i<rows.length; i++) {
            var row = rows[i];
            // John Tarr added to ignore rows that I've added the NoDrop attribute to (Header rows)
            var nodrop = row.getAttribute("NoDrop");
            if (nodrop == null || nodrop == "undefined") {  //There is no NoDrop attribute on rows I want to drag
                var rowY    = this.getPosition(row).y;
                var rowHeight = parseInt(row.offsetHeight)/2;
                if (row.offsetHeight == 0) {
                    rowY = this.getPosition(row.firstChild).y;
                    rowHeight = parseInt(row.firstChild.offsetHeight)/2;
                }
                // Because we always have to insert before, we need to offset the height a bit
                if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
                    // that's the row we're over

                    return row;
                }
            }
        }
        return null;
    }

The first, makeDraggable is called from the init function for each row in the table. It defines an onmousedown event handler to set the dragObject and the initial mouse offset (so we can track movements relative to the initial drag position). Now inside the onmousedown event handler that we are adding to each row, we want to be able to access the current TableDnD object, we can’t use this because it will be changed to the current object when the event handler is called, so instead we have a variable self which we bind to this outside the handler but which retains it’s value inside. Now the handler can refer directly to the methods and data on the TableDnD object. Inside the event handler we also need to check to see what the event source is, because if we capture and consume the onmousedown event for form elements, then the user won’t be able to click in them and type, or select. So in that case we have to just bubble it up by returning true.

As well as setting the onmousedown event handler, we also set the row’s cursor style to “move” so that the user can see it’s draggable.

The last method, findDropTargetRow works out the current row under the mouse. This is called as we move the mouse and is used to dynamically move the row so that the user can see what is happening. It simply iterates through the rows, getting the top and height of the rows and checks to see if the mouse is on it or not (in fact I displace the position by half a row to make it feel right when dragging, otherwise it’s very quick in one direction and seems “heavy” in the other direction—try different values to see what I mean!).

Again, John Tarr suggested that we should support no-drop zones too, so if the row has NoDrop set to true, then we just return null and the user can’t drop the row there

There is a browser problem however. Getting the offsetHeight for a row works fine in Internet Explorer (6 and 7) and Firefox (2.0.x), however Safari returns 0 for rows! Fortunately you can get the offsetHeight of a cell instead, so the code uses the row’s firstChild if the offsetHeight is zero. The same thing seems to happen for the rowY, so again I use the first cell.

In fact there was still a problem with Safari because the row’s offsetTop is also 0 (or sometimes a small number–presumably from the style). Thanks to Luis Chato for pointing this out and pointing me to this description of the problem. The same answer works again though. If the e.offsetHeight == 0 for the selected element in getPosition then we’ll just get the firstChild and use that instead. This works for all the browsers I’ve tested so far.

As well as the class we need one more global method to get the event source. IE and Firefox (and the others) do this in different ways. We could add this as an instance method on the TableDnD class, but because there is no TableDnD context needed, I decided to just make it a globally accessible method. Here it is:

/** get the source element from an event in a way that works for IE and Firefox and Safari
 * @param evt the source event for Firefox (but not IE--IE uses window.event) */

function getEventSource(evt) {
    if (window.event) {
        evt = window.event; // For IE
        return evt.srcElement;
    } else {
        return evt.target; // For Firefox
    }
}

Putting it all together

You can download the complete class and other methods from the resources below and then link to it from your web page. The next thing you need to do is to have a table, and then you can drag-enable the table by adding the following javascript either inline below the table HTML or in the document.onload event handler.

<script type="text/javascript">
var table = document.getElementById('table-1');
var tableDnD = new TableDnD();
tableDnD.init(table);
</script>

So all you need to do is create an instance of the TableDnD class, and then call init on it passing it the table you want to use. If you want to do something special when a row is dropped, you can add something like this:

// Redefine the onDrop so that we can display something
tableDnD.onDrop = function(table, row) {
    var rows = this.table.tBodies[0].rows;
    var debugStr = "rows now: ";
    for (var i=0; i<rows.length; i++) {
        debugStr += rows[i].id+" ";
    }
    document.getElementById('debug').innerHTML = 'row['+row.id+'] dropped<br>'+debugStr;
}

In fact if you look at the source of this page you can see how I implemented the debugging information displayed as you drag rows in the demonstration table at the beginning of this article.

Bells and whistles

Here’s a final example showing that you can have two separate tables on the same page, that you can support INPUTs
SELECTs, and header rows which aren’t draggable or droppable:

Label Value Input
Category 1
1 One
2 Two Two
3 Three Three
Category 2
4 Four
5 Five Five
6 Six

Note there is a problem with IE6 in that when you drag a row that has either a checkbox or a radio button in it, these get set back to their initial settings (well, it seems it’s a bit more complicated even than this–try it with the two radio buttons above). As far as I know this is fixed in IE7. If it’s crucial for you to support this, you would have to capture the state of the form elements on mouse down and then go through and reset them on mouse up. It only applies to radio buttons and check boxes on IE6, Safari and Firefox are fine, and select and text input tags are fine.

Resources

180 thoughts on “Dragging and dropping table rows in Javascript”

  1. Dear All,

    I really need your help!

    I am using this javascript code for reordering table rows and it works fine however i need to save reordered values to database table. I find myself unable to get the new position?

    For instance, we have three rows whose order is 1,2,3. and now i drag 3 over 1, it works but i do not see a way to get 3 was dragged over 1? Meaning, i have rows old positioning but i can’t update it into table since i do not have its new position?

    Can anyone guide please!

    Manisha

  2. This is an excellent tutorial. It works perfectly for table with static rows. Please help me how to implement to a table in which rows are created dynamically.

  3. Very good scripts 😀

    but i have a question?

    if i use js add new row of table row.

    How to apply drag and drop script to the new row?

    I try to recreate instant object .,but it doesn’t work

    can anyone help me??

    Big Thank

  4. After dragging and dropping one row to another position,
    the table.onDrop method gets called if I just click on a row, and don’t drop it on another row.

    How can I modify the script to prevent this behavior?

  5. Hi,

    When generating table rows on fly, then the drag and drop wont work.

    Somthing like this:
    {Content}

    {Content} is loaded dynamically.

    Can you say what to do to make it working?

    Thanks in advance,
    Amino

    1. If you want to address rows added dinamycally, just call the function makeDraggable to the row just added:

      var tableObj = document.getElementById(‘tableID′);
      var tableDnD = new TableDnD();
      tableDnD.init( tableObj );

      /* create a new row ( rowObj ) and add it dinamycally then call the function below */

      tableDnD.makeDraggable(rowObj);

  6. Hi,

    How do you save the current table of the table after you dragged? Must it always be passed to a persistence storage like a database? Is there a way to store it on the client side?

    1. Thanks 4 such gr8 article ….i hv 1 doubt in header row ,ma header row is also moving i knw we hv to add sum “NoDrag” attribute to header row but how ?m confused Please let me knw…waiting 4 reply

  7. HI,
    This is very fine..so much thanks.

    but i’m facing one issue here how can i group of rows at time please help me regrading this ASAP.

    once again this script is very good..

  8. very good script but useless for the most part ….
    after wasting more than 2 days trying to get this to work with dynamic tables I finally give up/
    why would anyone want to re arrange rows from a table already made and sitting on a page … cant think of any purpose really.
    the whole purpose of rearranging the rows is so it can be stored and recreated in the stored order …
    and since no one can change and save the html of another site to that site’s server then obviously the stored order has to be saved in a db and recreated…
    and when recreated dynamically it doesnt drag-drop …. useless/ thankx for the effort though.

  9. I want to use this javascript for drag and drop table rows. But after re-sorting table I want to save sorting settings to mysql database.
    How can I do this?

    Thanks

  10. Thanks for a great solution. May I make a feature suggestion though: please add the ability to autoscroll the page up or down while you are dragging a table row. I have a long table that scrolls off the page, and with your code I am not able to drag the row further if the table is off the screen.

  11. This code worked great for my Autofill extension for Google Chrome:

    https://chrome.google.com/extensions/detail/nlmmgnhgdeffjkdckmikfpnddkbbfkkk

    FYI, I also implemented autoscroll with the following additions:

    Get the document height with…

    nDocH = document.documentElement.clientHeight
    || document.body.clientHeight
    || window.innerHeight;

    Make sure you reset nDocH on window resize.

    Now add lines to implement the autoscroll…

    this.findDropTargetRow = function(y) {
    // Get document scroll height
    var nDocSH = (window.pageYOffset) ? window.pageYOffset
    : (D.documentElement) ? D.documentElement.scrollTop
    : (D.body) ? D.body.scrollTop
    : (window.scrollY) ? window.scrollY
    : 0;
    // Implement autoscroll
    if (y = nDocH + nDocSH – currenttable.mouseOffset.y – 40) scrollBy(0, 4);

    I added an optional 40px buffer so that the autoscroll would kick in before the cursor reaches the edge of the screen. You can increase/decrease the smoothness of the scrolling by changing the scrollBy X offset (4 works for me).

  12. Nice code. It solved my 80% problem. But i have two inner tables in main table how it will for me. For one table drag and drop is working fine

  13. Great work! But since some days I do have problems with dragging rows with content inside (link, image, …). I think there was an Windows or IE update that changed something in handling mouseDown/onClick. Dragging works only in empty areas of a cell/row now 🙁

  14. Thank you very much for a great plugin!

    I did need to modify it to allow altering a textarea. It was a quick change. Anyone running into the same problem simply replace the “this.makeDraggable” function with the following code:

    /** Take an item and add an onmousedown method so that we can make it draggable */
        this.makeDraggable = function(item) {
            if(!item) return;
            var self = this; // Keep the context of the TableDnd inside the function
            item.onmousedown = function(ev) {
                // Need to check to see if we are an input or not, if we are an input, then
                // return true to allow normal processing
                //Kelly McD, add check for textarea
                var target = getEventSource(ev);
                if (target.tagName == 'INPUT' || target.tagName == 'TEXTAREA' || target.tagName == 'SELECT') return true;
                currenttable = self;
                self.dragObject  = this;
                self.mouseOffset = self.getMouseOffset(this, ev);
                return false;
            }
            item.style.cursor = "move";
        }
    
  15. Tom’s solution for autoscroll works great to autoscroll the page but does any one have an idea how to get it to autoscroll if the drag/drop table is in a DIV with overflow on?
    Thanks!

  16. Amino and web masta mentioned problems using this class with tables created “on the fly” or “dynamically.” Although they didn’t provide the specifics of their setup, this class is working well for me with a table that is generated in a PHP script and loaded into the page via AJAX (by setting the innerHTML after the results come back from the PHP script). The only issue I see is that you have to wait until the table is loaded into the page before initializing the tableDnD object. I simply call the initialization from my AJAX callback function and everything is fine.

    I also used Kelly McD’s technique above for allowing editable textareas in the table rows.

    To those who have asked how to save the state of the table after reordering, here’s what I did:

    1) Set the id of each table row, when generating the table, to the corresponding database ID.

    2) In the tableDnD.onDrop method, looped through table.rows, got the id of each row and added those to an array of IDs.

    3) When a user saves the page, I change the IDs array to a comma-delimited string and put it into a hidden field that is submitted with some other form fields for database storage. If you’re not submitting a form, you could just send this string to another PHP script via AJAX instead. You could also save this string into a cookie for local storage.

    I also use alternating background colors on my table rows, so in tableDnD.onDrop, while I’m looping through the rows, I update the className of each row to update the colors for the new row order.

  17. Anybody knows how to implement this script in Drupal 7? I want to use it instead of drupal_add_tabledrag?

    Jquery is standard implemented with Drupal 7. I have added the specific js:
    drupal_add_js(‘sites/all/files/js/jquery.tablednd_0_5.js’);

    I have created a table with theme table, giving the table a unique name and rowid’s

    but it does not want to work

  18. Anyone knows why we get this nullpointerexception on the ‘e.offsetParent’ loop. This apparently happens when you create dynamic tables.

  19. that isn’t a good tutorial. It is not userfriendly and not to use for beginners and people who are NOT JS-Profies.

    It explains something “around” and much “tralala” but not how to use.
    There is no full example – no download of an example and the Link in “Puttin in all together” -> “You can download the complete class and other methods from the resources below” -> http://www.isocra.com/#resources links only on a site where nothing is to download.
    I think it is nearly useless.
    Even you can’t copy it from the sourcecode of this site because there are a hundred of other includes you don’t know which one to use.

    there is no hint that you must write something like this:

    function start() // Here you must write it as a function that you can call it on onLoad !!!
    {
    var table = document.getElementById(‘table-1’);
    var tableDnD = new TableDnD();
    tableDnD.init(table);
    }

    Even there is no information how to adress the new order for saving it into a database 🙁

  20. I would add the code:
    if (ev && ev.preventDefault)
    ev.preventDefault();

    to the mousedown event. without it, FF4 was selecting the text that was in the table while I was moving a row.

    1. Bill, of course you can save the moves to a database, but how you do it is up to you. You could have a form and when the user presses the save button you could interrogate the table to see what the rows are and save the order then. Or you could detect the row drop event and use Ajax to tell the server what the new order is. It depends a lot on what you’re building and what the user will expect to happen.

      1. Thanks Denis. I’m not that familiar with Ajax, etc. so I’ll have to do some research. The main thing I’m wondering about is id number for the table rows. I am working with a page that is a repeat region of the first row in the table to auto create the rest of the rows so I’m not sure how to handle that.

        1. Bill, you’ll need to have some way of giving all the rows a unique identifier otherwise there’s no easy way to get the new order. If you’re repeating the first row using JavaScript, then you need to get it to put unique identifiers on the rows as it goes along. Ditto if it’s generated from Cold Fusion server-side. If you’ve got form elements in the table rows and you’re submitting the whole form, then you should get multiple values for each of the inputs and the order that you receive them should tell you which order the rows were in.

          In general, I would use our jQuery TableDnD plug-in rather than rolling your own JavaScript, it’s much easier and jQuery gives you so much more.

Leave a Reply

Your email address will not be published. Required fields are marked *