Thursday, February 18, 2016

Sorting of table rows ( Drag and Drop )

HTML

First, we build a table with some data in it:
<table class="table" id="diagnosis_list">
    <thead>
        <tr><th>Priority</th><th>Name</th><th>Favorite fruit</th><th>Vegetarian?</th><th>&nbsp;</th></tr>
    </thead>
    <tbody>
        <tr><td class='priority'>1</td><td>George Washington</td><td>Apple</td><td>N</td><td><a class='btn btn-delete btn-danger'>Delete</a></td></tr>
        <tr><td class='priority'>2</td><td>John Adams</td><td>Pear</td><td>Y</td><td><a class='btn btn-delete btn-danger'>Delete</a></td></tr>
        <tr><td class='priority'>3</td><td>Thomas Jefferson</td><td>Banana</td><td>Y</td><td><a class='btn btn-delete btn-danger'>Delete</a></td></tr>
        <tr><td class='priority'>4</td><td>Ben Franklin</td><td>Kumquat</td><td>N</td><td><a class='btn btn-delete btn-danger'>Delete</a></td></tr>
        <tr><td class='priority'>5</td><td>Alexander Hamilton</td><td>Red grapes</td><td>N</td><td><a class='btn btn-delete btn-danger'>Delete</a></td></tr>
    </tbody>
</table>
It’s just a normal table, with an ID and explicit <thead> and <tbody> sections. This lets us make the <tbody> sortable, while leaving the <thead> alone. Also note that the first <td> in each row has the class “priority”. This is where our javascript will write each row’s priority number.

CSS

The demo includes basic Bootstrap styles for overall look and feel. So we only require a little bit of custom CSS to make the interactions more enjoyable.
.ui-sortable tr {
    cursor:pointer;
}    
.ui-sortable tr:hover {
    background:rgba(244,251,17,0.45);
}
When hovering over a draggable table row, this CSS displays the hand cursor and turns the background of the row a transparent yellow.

Javascript

To get the sortable behavior, we call in the jQuery UI Javascript file. Note that we don’t call in the jQuery UI CSS: we want the functionality, not the styling.
Then we add the following code:
$(document).ready(function() {
    //Helper function to keep table row from collapsing when being sorted
    var fixHelperModified = function(e, tr) {
        var $originals = tr.children();
        var $helper = tr.clone();
        $helper.children().each(function(index)
        {
          $(this).width($originals.eq(index).width())
        });
        return $helper;
    };

    //Make diagnosis table sortable
    $("#diagnosis_list tbody").sortable({
        helper: fixHelperModified,
        stop: function(event,ui) {renumber_table('#diagnosis_list')}
    }).disableSelection();

    //Delete button in table rows
    $('table').on('click','.btn-delete',function() {
        tableID = '#' + $(this).closest('table').attr('id');
        r = confirm('Delete this item?');
        if(r) {
            $(this).closest('tr').remove();
            renumber_table(tableID);
            }
    });
});

//Renumber table rows
function renumber_table(tableID) {
    $(tableID + " tr").each(function() {
        count = $(this).parent().children().index($(this)) + 1;
        $(this).find('.priority').html(count);
    });
}
This is the core of the demo, so let’s go through it in detail.
    //Make diagnosis table sortable
    $("#diagnosis_list tbody").sortable({
        helper: fixHelperModified,
        stop: function(event,ui) {renumber_table('#diagnosis_list')}
    }).disableSelection();
This calls the jQuery UI “sortable” method on the <tbody> element of the table. It includes a helper function called fixHelperModified. Once the sort is finished, it calls the renumber_table() function.
What is the point of fixHelperModified? I’m so glad you asked:
    //Helper function to keep table row from collapsing when being sorted
    var fixHelperModified = function(e, tr) {
        var $originals = tr.children();
        var $helper = tr.clone();
        $helper.children().each(function(index)
        {
          $(this).width($originals.eq(index).width())
        });
        return $helper;
    };
The best way to see what this function does is to execute the sortable call without it. By default, any table row you drag will collapse down to minimum size, leaving you with a strange looking table:
bad_sortable_table
The helper function maintains the full width of the row, avoiding a weird visual experience while dragging. Thanks to Brian Grinstead for making this JSFiddle that illustrates the problem and a couple of different solutions, as originally raised on Stack Overflow.
When dragging is done, it triggers the renumber_table() function:
//Renumber table rows
function renumber_table(tableID) {
    $(tableID + " tr").each(function() {
        count = $(this).parent().children().index($(this)) + 1;
        $(this).find('.priority').html(count);
    });
}
This puts all the table rows into a jQuery object, then uses jQuery’s .each() method to loop through each row and renumber them based on their position in the table. “count” is the index of each row; the code adds 1 to this because indexes start at 0, and we want to start at one. Then it replaces the content of the “priority” <td> with count.

Bonus: Delete a table row

You may have noticed I skipped a code block:
    //Delete button in table rows
    $('table').on('click','.btn-delete',function() {
        tableID = '#' + $(this).closest('table').attr('id');
        r = confirm('Delete this item?');
        if(r) {
            $(this).closest('tr').remove();
            renumber_table(tableID);
            }
    });
As the note says, this code is triggered when a row’s “Delete” button is clicked. It does several things:
  1. Gets the ID of the table;
  2. Shows a confirmation dialog asking if you really want to delete the row;
  3. If you confirm the deletion, it finds the <tr> tag for the row the button is in, deletes the row, and then calls the renumber_table() function to, uh, renumber the table. This last is why it grabbed the ID of the table in Step 1; it needs to pass the ID to the renumbering function.