Drag and Drop table content with JavaScript

Content of HTML table cells can be dragged to another cell or another table. It isn’t difficult to define onMouseMove handler and change top / left element styles to move the object. In case with tables, you will have to determine somehow target cell. Attaching onMouseOver handler on TD elements will not work, because browser doesn’t fire events to the elements below the dragged object.

Anyway, after taking care of the current scroll position and calculating TD positions, REDIPS.drag should work in recent major browsers like Google Chrome, Firefox, Safari, Internet Explorer, Opera and mobile devices as well. Click on image below, will open live demo where you can drag green, blue or orange bordered DIV elements, change properties (radio button and check-boxes) and click on “Save” button.

Download latest version redips2.tar.gz


REDIPS.drag example01

In this example “Save” button will scan table content, create query string and send to PHP page. Demo shows how to collect content and accept parameters on the server side. More about accepting parameters you can read at Reading multiple parameters in PHP. “Clone” elements (orange in this demo) will be duplicated first because of “redips-clone” keyword contained in class name. If you drop object on cell named “Trash”, object will be deleted from the table (with or without confirmation). Library has built in autoscroll and option to forbid landing to non empty cells or cells named with class “redips-mark”. Table can contain rowspan / colspan TDs and different background color for every cell.

Here are minimal steps to enable content dragging in table:

  • put <script type=”text/javascript” src=”redips-drag-min.js”></script> to the head section
  • initialize REDIPS.drag library: <body onload=”REDIPS.drag.init()”>
  • place table(s) inside <div id=”redips-drag”> to enable content dragging
  • place <div class=”redips-drag”>Hello World</div> to the table cell

Other features of REDIPS.drag library:

  • methods and data structure are defined in namespace (easier integration with other JS frameworks)
  • all JavaScript code is checked with ESLint
  • REDIPS.drag documentation generated with JsDoc Toolkit
  • drag and drop table rows
  • movable DIV element can contain other HTML code (images, forms, tables …)
  • forbidding or allowing TDs marked with class name “redips-mark”
  • option to define exceptions and allow dropping certain DIV elements to the marked cell
  • option to define single content cell on the table declared with “multiple” drop option
  • cloning
    • for unlimited cloning add “redips-clone” class name to the DIV object
      <div class=”redips-drag redips-clone”>Hello World</div>
    • to limit cloning and transform last object to the ordinary movable object add ‘climit1_X’ class name
      <div class=”redips-drag redips-clone climit1_4″>Hello World</div>
    • to limit cloning and transform last object to immovable object add ‘climit2_X’ class name
      <div class=”redips-drag redips-clone climit2_4″>Hello World</div>
    • where X is integer and defines number of cloned elements (in previous examples, each climit will allow only 4 cloned elements)
  • unlimited nested tables support
  • dropping objects only to empty cells
  • switch cell content
  • switching cell content continuously
  • overwrite TD content with dropped element
  • shift table content
  • table cell with “redips-trash” class name becomes trashcan
  • enabled handlers to place custom code on events: changed, clicked, cloned, clonedDropped, clonedEnd1, clonedEnd2, dblClicked, deleted, dropped, droppedBefore, finish, moved, notCloned, notMoved, shiftOverflow, relocateBefore, relocateAfter, relocateEnd, rowChanged, rowClicked, rowCloned, rowDeleted, rowDropped, rowDroppedBefore, rowDroppedSource, rowMoved, rowNotCloned, rowNotMoved, rowUndeleted, switched and undeleted
  • deleting cloned DIV if the cloned DIV is dragged outside of any table
  • enabling / disabling dragging
  • animation (move element/row to the destination cell/row)
  • added support for touch devices (touchstart, touchmove, touchend)

How REDIPS.drag works?

Script will search for DIV elements (with class name “redips-drag”) inside tables closed in <div id=”redips-drag”> and attach onMouseDown event handler. When user clicks with left mouse button on DIV element, onMouseMove and onMouseUp handlers will be attached to the document level.

While dragging DIV element, script changes its “left” and “top” styles. This is function of the onMouseMove handler. When user releases left mouse button, onMouseUp event handler will unlink onMouseMove and onMouseUp event handlers. This way, browser will listen and process mousemove events only when DIV element is dragged.

As I mentioned, onMouseDown is defined on the elements you want to drag. Elements beneath the dragged object will not be able to catch onMouseOver event. Why? Because you are dragging object and that object only can catch the onMouseOver event.

So, to detect destination table cells, script calculates all cell coordinates (with scroll page offset) and store them to the array. Array is searched inside onMouseMove handler and after left mouse button is released, DIV will drop to the current (highlighted) table cell.

In redips2.tar.gz package you will find many examples including example of how to save/recall table using PHP and MySQL. Package also contains and redips-drag-min.js – a compressed version of REDIPS.drag library (compressed with Google Closure Compiler).

Happy dragging and dropping!

1,195 thoughts on “Drag and Drop table content with JavaScript”

  1. Darko,

    Nice library. Are there any save or load functions to persists table configuration over multiple sessions? Thanks.

    Goran

  2. Great stuff! I too am interested in functions that would allow for saving and loading the table configuration across sessions. So, for example, a user drags content to row 2, column 1, I would like to save that infomation (probably in MySQL) and then the next time the user goes to the page the content displays in that same location. Any update on that?

  3. @Goran & Scott – REDIPS.drag has built-in save_content() method. Input parameters are table id (or table reference) and data type: query string or JSON. So, for defined table, method will scan table and return DIV elements plus coordinates in query string or JSON format. This info can be send to the server and saved to the database. Next time position of DIV elements can be read directly from database and displayed dynamically on the page. Similar is shown in example03: School timetable

    On the other side, each move can be saved in background via AJAX. Please download redips2.tar.gz and go to the example03 directory. There you will find readme.txt file with instructions how to prepare local LAMP server to test saving table content to the MySQL database.

  4. Hi Darko,

    Thx for your library! Im a bit experimenting with it, to build a schedule. Nearly everything works as expected, except for one thing :-) :

    When the schedule is in a scrollable div and the content of the div is larger than the div itself, dragging and dropping items is very difficult when the user has scrolled down on the div. When the user drops the item, the item drop at the place where the cursor would be if the user hadn’t scrolled, simple example that explains the problem:

    http://jsbin.com/anohid/19/edit#html,live

    After scrolling on the div, dropping items is very hard. Is there a sollution for this?

    Thanks in advance!

  5. @Rub3s – First thank you for preparing example to show where is the problem – very nice. OK, in the following line:

    <div id="right" style="height:300px; width:350px; overflow-y:scroll;">
    

    add style postition:relative and highlighting should return in place:

    <div id="right" style="height:300px; width:350px; overflow-y:scroll; position:relative;">
    

    The problem is in element position calculation. More details can be found here (Element position with scrolling offsets):

    http://www.howtocreate.co.uk/tutorials/javascript/browserspecific

    … make sure that every element with an overflow of anything other than “visible” also has a position style set to something other than the default “static”. This way, they will all appear in the offsetParent chain, and can be easily subtracted in the same loop that adds the offsetLeft and offsetTop.

  6. Hello (sorry for the translation I’m French), great work on this script!

    http://img846.imageshack.us/img846/4559/dragiy.png

    I have DIV stacked in columns (machines), each DIV in a TD.
    When I move the DIV 1 to DIV 5, I like that DIV 2, 3, and 4 moves up one line (never a blank line).
    I added a function (SupprVides();) which works but is very slow when there are hundreds of DIV!
    Have you any idea to automatically move many DIV at same time?
    Thank you very much.
    Manu.

  7. @MagicManu – If you want simpler method to fill the gap after DIV element is dropped to other cell here it complete redips_init() method:

    // redips initialization
    redips_init = function () {
        // reference to the REDIPS.drag library
        var rd = REDIPS.drag;
        // initialization
        rd.init();
        // set drop option to "shift"
        rd.drop_option = 'shift';
        // each column is treated separately
        rd.shift_option = 'vertical2';
        // enable animation on shifted elements
        // in case of many DIV elements, try with "false"
        rd.animation_shift = true;
        // event handler invoked after DIV element is dropped
        rd.myhandler_dropped = function (target_cell) {
            // find reference for source and target tables / columns
            var tbl_source = rd.find_parent('TABLE', rd.source_cell),
                tbl_target = rd.find_parent('TABLE', target_cell),
                idx_source = rd.source_cell.cellIndex,
                idx_target = target_cell.cellIndex;
            // shift DIVs in source column if target cell is different than source cell
            // (exclude drag and drop in the same column)
            if (rd.source_cell !== target_cell && !(tbl_source === tbl_target && idx_source === idx_target)) {
                // shift cells in source column
                rd.shift_cells(rd.source_cell, rd.find_cell('lastInColumn', rd.source_cell)[2]);
            }
        };
    };
    

    To make it work, please download version 4.6.13 of REDIPS.drag because find_cell() method was not exposed as public. In case of many DIV element placed in table it also may be sluggish (especially in IE8 browser). IE8 is slow browser so there’s not much to do. But, animation can be turned off and that can speed up element relocation. In above example, try with:

    rd.animation_shift = false;
    

    Hope this code snippet will be helpful.

  8. Thank you very much, it works very well except for this case:
    I have several empty cell at the bottom of each table (to add new div). If I move a DIV in the last empty cell of a column, the source cell leave blank.

    Currently, I delete the TR source:

    if (rd.source_cell !== rd.target_cell && !(tbl_source === tbl_target && idx_source === idx_target)) {
        // shift cells in source column
        rd.shift_cells(rd.source_cell, rd.find_cell('lastInColumn', rd.source_cell)[2]);
    }
    // remove TR if empty
    else if (rd.source_cell.innerHTML=='')
        rd.source_cell.parentNode.parentNode.removeChild(rd.source_cell.parentNode);
    

    Do you have a better idea?
    Thank you.

  9. Hi Darko, you created a really useful library, great job.

    I have been experimenting with example 3, and wrapped table1 into a div. Made that div draggable (using jQuery UI) and gave it a higher z-index then table2. This means that you can place table1 over (or on top of) table2. All works well on a desktop.

    But if you use a tablet (or worse a phone) the draggable cells from table1 move very shocking, I think this is due to the limited processor power of a tablet, and lots of checks that get fired by handler_onmousemove when hovering over 2 tables at once.

    Is there a way to use the z-index of a table (or its parent) to eliminate the extra checks for the other table?

  10. @MagicManu – JS code from previous comment will not shift DIV elements up if DIV element is moved inside the same column. If deleting row (TR) fulfill rules of your application then it is OK. I would only suggest to use hasChildNodes() to test if DOM node is empty:

    else if (!rd.source_cell.hasChildNodes())
    

    But beware in Mozilla this will return true if there is whitespace after the tag (hasChildNodes() calculates any child node no matter of node type).

    @maniosus – If “shocking behaviour” you mentioned means jittering then the reason may not be in lack of CPU power but in difference between desktop and tablet browser. It seems that REDIPS.drag needs more polishing in case of overlaying tables on tablet PC. Unfortunately, I didn’t test REDIPS.drag on tablets yet, but I hope I will soon. Anyway, thanks for pointing to the problem.

  11. Thanks for the quick reply. Could you please explain what differences you are referring to? Because I found that most differences have been resolved lately, the only big difference I notice is the automatic resizing of pages to fit the screen.

    Also I tested REDIPS.drag in the default android 4.0 browser, opera mobile, firefox and dolphin. In all browsers the jitter is there in the overlapping tables. But it is more then just jitter. The floating cell start moving really slow, and has problems catching up with your finger. But once it has caught up, it works fine (untill you pass over the overlapping tables again).

    The rest of the functionalities of REDIPS.drag work as expected.

  12. Nice library! Is there any way once an item has been dragged and dropped you can prevent the target from being moved? (I have overwrite on so it doesn’t limit users much). Thanks!

  13. @maniosus – Maybe you’re right, I never tested REDIPS.drag on a slow desktop computer (like some PC with 1GHz CPU) so I’m not 100% sure. Code optimization is always more than welcome. I will be very grateful if you can send me a zipped example03 with jQuery modification to start with debugging – thanks!

    @Ron – Yes, please see example02 where DIV elements are set as immovable if dropped to green/orange cells. You should use REDIPS.drag.enable_drag() method inside myhandler_dropped() event handler. Something like:

    // define redips_init variable
    var redips_init;
    
    // redips initialization
    redips_init = function () {
        // reference to the REDIPS.drag lib
        var rd = REDIPS.drag;
        // initialization
        rd.init();
        // this function (event handler) is called after element is dropped
        rd.myhandler_dropped = function () {
            // make dropped DIV element immovable
            rd.enable_drag(false, rd.obj.id);
        };
    };
    
    // add onload event listener
    if (window.addEventListener) {
        window.addEventListener('load', redips_init, false);
    }
    else if (window.attachEvent) {
        window.attachEvent('onload', redips_init);
    }
    
  14. Hi Darko,

    I have narrowed down the problem. It is with the autoscroll function. If I disable autoscroll (by adding the “noautoscroll” class) dragging works fine.

    I think autoscrolling is a nice feature, but now it is implemented in a way that it accelerates when you come closer to the edge. This uses a lot of calculations. How about when you make the acceleration optional? So a default scrolling speed when you enter the bound size if the acceleration is disabled.

    Also on my page the tables are all fully visible, there are no scrollbars. So the real question is why does the autoscrollX and Y functions get called. It would be a great optimization if you knew if a table had to be scrolled. And if not, treat it as a noautoscroll table.

  15. I am dragging and dropping from a table with cells containing large amounts of wrapped text. When I pick up a cell, the text expands in width to almost the width of the screen. I wanted to keep it the same width as the cell was being moved around, so I added this to my REDIPS setup :-

    var myomy;
    REDIPS.drag.myhandler_clicked = function () {
        myomy = REDIPS.drag.obj.offsetWidth;
    }
    REDIPS.drag.myhandler_moved = function () {
        REDIPS.drag.obj.style.width = String(myomy - 6) + 'px';
    }
    

    It seems to work until I drop the cell into another table cell that has a different width or height to the original. Despite the table stipulating that it is center-aligned, the text is always left-aligned. Any suggestions?

  16. @maniosus – Thank you for narrowing down the problem, now it’s much easier to discuss. Calculation of scroll acceleration has smaller impact to performance comparing to calling calculate_cells() and set_trc() private methods called inside autoscrollX and autoscrollY.

    I added REDIPS.drag.autoscroll public property with default value true. Please download version 4.6.14 and try to set this property to false after lib initialization. This will completely turn off autoscrolling and speed up library. Documentation will be updated later today.

    @Mark Jacobs – It seems that dragged DIV element inherits styles from other DOM nodes because some styles are not defined. Maybe you only need to define CSS class (with width) and set to the DIV element so width style will not change in drag&drop process. Your approach is good for cases where is needed style change on the fly but probably this is not the case here.

  17. Thanks Darko. I have solved the problem with:

    var myomy;
    REDIPS.drag.myhandler_clicked = function (cucl) {
        myomy = cucl.offsetWidth;
    }
    REDIPS.drag.myhandler_moved = function () {
        REDIPS.drag.obj.style.width = String(myomy - 6) + 'px';
    }
    REDIPS.drag.myhandler_dropped = function (cucl) {
        REDIPS.drag.obj.style.width = '';
    }
    

    Setting the width style back to blank fits it back into the destination cell perfectly. Your hint about the width style being set and not changed during the drag and drop process, helped me figure out how to solve my particular predicament. BTW, this is the best D&D package out there, IMHO!

  18. Redips works perfectly for me except in one situation.
    I have two tables (0 and 1). When dragging a div from table 1 over a cell in table 0, the target cell reacts as expected as long as table 1 doesn’t is scrolled under and above table 0.

    If table 1 is scrolled so that the top border of table 1 is above the top border of table 0 the target cell in table 0 doesn’t react at all. It seems that in such a case the mouseover event stays in table 1.

    Is there a possible solution for that?

    Regards

    /Kjell

Leave a Comment