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@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 borderâwell 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 */ 59jQuery.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 310jQuery.fn.extend( 311 { 312 tableDnD : jQuery.tableDnD.build 313 } 314);