Home | History | Annotate | Download | only in third_party
      1 /**
      2  * TableDnD plug-in for JQuery, allows you to drag and drop table rows
      3  * You can set up various options to control how the system will work
      4  * Copyright  Denis Howlett <denish (at) isocra.com>
      5  * Licensed like jQuery, see http://docs.jquery.com/License.
      6  *
      7  * Configuration options:
      8  *
      9  * onDragStyle
     10  *     This is the style that is assigned to the row during drag. There are limitations to the styles that can be
     11  *     associated with a row (such as you can't assign a borderwell you can, but it won't be
     12  *     displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
     13  *     a map (as used in the jQuery css(...) function).
     14  * onDropStyle
     15  *     This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
     16  *     to what you can do. Also this replaces the original style, so again consider using onDragClass which
     17  *     is simply added and then removed on drop.
     18  * onDragClass
     19  *     This class is added for the duration of the drag and then removed when the row is dropped. It is more
     20  *     flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
     21  *     is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
     22  *     stylesheet.
     23  * onDrop
     24  *     Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
     25  *     and the row that was dropped. You can work out the new order of the rows by using
     26  *     table.rows.
     27  * onDragStart
     28  *     Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
     29  *     table and the row which the user has started to drag.
     30  * onAllowDrop
     31  *     Pass a function that will be called as a row is over another row. If the function returns true, allow
     32  *     dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
     33  *     the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
     34  * scrollAmount
     35  *     This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
     36  *     window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
     37  *     FF3 beta)
     38  *
     39  * Other ways to control behaviour:
     40  *
     41  * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
     42  * that you don't want to be draggable.
     43  *
     44  * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
     45  * <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
     46  * an ID as must all the rows.
     47  *
     48  * Known problems:
     49  * - Auto-scoll has some problems with IE7  (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
     50  *
     51  * Version 0.2: 2008-02-20 First public version
     52  * Version 0.3: 2008-02-07 Added onDragStart option
     53  *                         Made the scroll amount configurable (default is 5 as before)
     54  * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
     55  *                         Added onAllowDrop to control dropping
     56  *                         Fixed a bug which meant that you couldn't set the scroll amount in both directions
     57  *                         Added serialise method
     58  */
     59 jQuery.tableDnD = {
     60     /** Keep hold of the current table being dragged */
     61     currentTable : null,
     62     /** Keep hold of the current drag object if any */
     63     dragObject: null,
     64     /** The current mouse offset */
     65     mouseOffset: null,
     66     /** Remember the old value of Y so that we don't do too much processing */
     67     oldY: 0,
     68 
     69     /** Actually build the structure */
     70     build: function(options) {
     71         // Make sure options exists
     72         options = options || {};
     73         // Set up the defaults if any
     74 
     75         this.each(function() {
     76             // Remember the options
     77             this.tableDnDConfig = {
     78                 onDragStyle: options.onDragStyle,
     79                 onDropStyle: options.onDropStyle,
     80 				// Add in the default class for whileDragging
     81 				onDragClass: options.onDragClass ? options.onDragClass : "tDnD_whileDrag",
     82                 onDrop: options.onDrop,
     83                 onDragStart: options.onDragStart,
     84                 scrollAmount: options.scrollAmount ? options.scrollAmount : 5
     85             };
     86             // Now make the rows draggable
     87             jQuery.tableDnD.makeDraggable(this);
     88         });
     89 
     90         // Now we need to capture the mouse up and mouse move event
     91         // We can use bind so that we don't interfere with other event handlers
     92         jQuery(document)
     93             .bind('mousemove', jQuery.tableDnD.mousemove)
     94             .bind('mouseup', jQuery.tableDnD.mouseup);
     95 
     96         // Don't break the chain
     97         return this;
     98     },
     99 
    100     /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
    101     makeDraggable: function(table) {
    102         // Now initialise the rows
    103         var rows = table.rows; //getElementsByTagName("tr")
    104         var config = table.tableDnDConfig;
    105         for (var i=0; i<rows.length; i++) {
    106             // To make non-draggable rows, add the nodrag class (eg for Category and Header rows)
    107 			// inspired by John Tarr and Famic
    108             var nodrag = $(rows[i]).hasClass("nodrag");
    109             if (! nodrag) { //There is no NoDnD attribute on rows I want to drag
    110                 jQuery(rows[i]).mousedown(function(ev) {
    111                     if (ev.target.tagName == "TD") {
    112                         jQuery.tableDnD.dragObject = this;
    113                         jQuery.tableDnD.currentTable = table;
    114                         jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
    115                         if (config.onDragStart) {
    116                             // Call the onDrop method if there is one
    117                             config.onDragStart(table, this);
    118                         }
    119                         return false;
    120                     }
    121                 }).css("cursor", "move"); // Store the tableDnD object
    122             }
    123         }
    124     },
    125 
    126     /** Get the mouse coordinates from the event (allowing for browser differences) */
    127     mouseCoords: function(ev){
    128         if(ev.pageX || ev.pageY){
    129             return {x:ev.pageX, y:ev.pageY};
    130         }
    131         return {
    132             x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
    133             y:ev.clientY + document.body.scrollTop  - document.body.clientTop
    134         };
    135     },
    136 
    137     /** Given a target element and a mouse event, get the mouse offset from that element.
    138         To do this we need the element's position and the mouse position */
    139     getMouseOffset: function(target, ev) {
    140         ev = ev || window.event;
    141 
    142         var docPos    = this.getPosition(target);
    143         var mousePos  = this.mouseCoords(ev);
    144         return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
    145     },
    146 
    147     /** Get the position of an element by going up the DOM tree and adding up all the offsets */
    148     getPosition: function(e){
    149         var left = 0;
    150         var top  = 0;
    151         /** Safari fix -- thanks to Luis Chato for this! */
    152         if (e.offsetHeight == 0) {
    153             /** Safari 2 doesn't correctly grab the offsetTop of a table row
    154             this is detailed here:
    155             http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
    156             the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
    157             note that firefox will return a text node as a first child, so designing a more thorough
    158             solution may need to take that into account, for now this seems to work in firefox, safari, ie */
    159             e = e.firstChild; // a table cell
    160         }
    161 
    162         while (e.offsetParent){
    163             left += e.offsetLeft;
    164             top  += e.offsetTop;
    165             e     = e.offsetParent;
    166         }
    167 
    168         left += e.offsetLeft;
    169         top  += e.offsetTop;
    170 
    171         return {x:left, y:top};
    172     },
    173 
    174     mousemove: function(ev) {
    175         if (jQuery.tableDnD.dragObject == null) {
    176             return;
    177         }
    178 
    179         var dragObj = jQuery(jQuery.tableDnD.dragObject);
    180         var config = jQuery.tableDnD.currentTable.tableDnDConfig;
    181         var mousePos = jQuery.tableDnD.mouseCoords(ev);
    182         var y = mousePos.y - jQuery.tableDnD.mouseOffset.y;
    183         //auto scroll the window
    184 	    var yOffset = window.pageYOffset;
    185 	 	if (document.all) {
    186 	        // Windows version
    187 	        //yOffset=document.body.scrollTop;
    188 	        if (typeof document.compatMode != 'undefined' &&
    189 	             document.compatMode != 'BackCompat') {
    190 	           yOffset = document.documentElement.scrollTop;
    191 	        }
    192 	        else if (typeof document.body != 'undefined') {
    193 	           yOffset=document.body.scrollTop;
    194 	        }
    195 
    196 	    }
    197 
    198 		if (mousePos.y-yOffset < config.scrollAmount) {
    199 	    	window.scrollBy(0, -config.scrollAmount);
    200 	    } else {
    201             var windowHeight = window.innerHeight ? window.innerHeight
    202                     : document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight;
    203             if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) {
    204                 window.scrollBy(0, config.scrollAmount);
    205             }
    206         }
    207 
    208 
    209         if (y != jQuery.tableDnD.oldY) {
    210             // work out if we're going up or down...
    211             var movingDown = y > jQuery.tableDnD.oldY;
    212             // update the old value
    213             jQuery.tableDnD.oldY = y;
    214             // update the style to show we're dragging
    215 			if (config.onDragClass) {
    216 				dragObj.addClass(config.onDragClass);
    217 			} else {
    218 	            dragObj.css(config.onDragStyle);
    219 			}
    220             // If we're over a row then move the dragged row to there so that the user sees the
    221             // effect dynamically
    222             var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y);
    223             if (currentRow) {
    224                 // TODO worry about what happens when there are multiple TBODIES
    225                 if (movingDown && jQuery.tableDnD.dragObject != currentRow) {
    226                     jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling);
    227                 } else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) {
    228                     jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow);
    229                 }
    230             }
    231         }
    232 
    233         return false;
    234     },
    235 
    236     /** We're only worried about the y position really, because we can only move rows up and down */
    237     findDropTargetRow: function(draggedRow, y) {
    238         var rows = jQuery.tableDnD.currentTable.rows;
    239         for (var i=0; i<rows.length; i++) {
    240             var row = rows[i];
    241             var rowY    = this.getPosition(row).y;
    242             var rowHeight = parseInt(row.offsetHeight)/2;
    243             if (row.offsetHeight == 0) {
    244                 rowY = this.getPosition(row.firstChild).y;
    245                 rowHeight = parseInt(row.firstChild.offsetHeight)/2;
    246             }
    247             // Because we always have to insert before, we need to offset the height a bit
    248             if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
    249                 // that's the row we're over
    250 				// If it's the same as the current row, ignore it
    251 				if (row == draggedRow) {return null;}
    252                 var config = jQuery.tableDnD.currentTable.tableDnDConfig;
    253                 if (config.onAllowDrop) {
    254                     if (config.onAllowDrop(draggedRow, row)) {
    255                         return row;
    256                     } else {
    257                         return null;
    258                     }
    259                 } else {
    260 					// If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
    261                     var nodrop = $(row).hasClass("nodrop");
    262                     if (! nodrop) {
    263                         return row;
    264                     } else {
    265                         return null;
    266                     }
    267                 }
    268                 return row;
    269             }
    270         }
    271         return null;
    272     },
    273 
    274     mouseup: function(e) {
    275         if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) {
    276             var droppedRow = jQuery.tableDnD.dragObject;
    277             var config = jQuery.tableDnD.currentTable.tableDnDConfig;
    278             // If we have a dragObject, then we need to release it,
    279             // The row will already have been moved to the right place so we just reset stuff
    280 			if (config.onDragClass) {
    281 	            jQuery(droppedRow).removeClass(config.onDragClass);
    282 			} else {
    283 	            jQuery(droppedRow).css(config.onDropStyle);
    284 			}
    285             jQuery.tableDnD.dragObject   = null;
    286             if (config.onDrop) {
    287                 // Call the onDrop method if there is one
    288                 config.onDrop(jQuery.tableDnD.currentTable, droppedRow);
    289             }
    290             jQuery.tableDnD.currentTable = null; // let go of the table too
    291         }
    292     },
    293 
    294     serialize: function() {
    295         if (jQuery.tableDnD.currentTable) {
    296             var result = "";
    297             var tableId = jQuery.tableDnD.currentTable.id;
    298             var rows = jQuery.tableDnD.currentTable.rows;
    299             for (var i=0; i<rows.length; i++) {
    300                 if (result.length > 0) result += "&";
    301                 result += tableId + '[]=' + rows[i].id;
    302             }
    303             return result;
    304         } else {
    305             return "Error: No Table id set, you need to set an id on your table and every row";
    306         }
    307     }
    308 }
    309 
    310 jQuery.fn.extend(
    311 	{
    312 		tableDnD : jQuery.tableDnD.build
    313 	}
    314 );