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 (c) Denis Howlett <denish@isocra.com>
5 * Licensed like jQuery, see http://docs.jquery.com/License.
7 * Configuration options:
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).
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.
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
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
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.
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.
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,
39 * This is a jQuery mach string for one or more cells in each row that is draggable. If you
40 * specify this, then you are responsible for setting cursor: move in the CSS and only these cells
41 * will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
42 * the whole row is draggable.
44 * Other ways to control behaviour:
46 * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
47 * that you don't want to be draggable.
49 * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
50 * <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
51 * an ID as must all the rows.
55 * $("...").tableDnDUpdate()
56 * Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
57 * This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
58 * The table maintains the original configuration (so you don't have to specify it again).
60 * $("...").tableDnDSerialize()
61 * Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
62 * called from anywhere and isn't dependent on the currentTable being set up correctly before calling
65 * - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
67 * Version 0.2: 2008-02-20 First public version
68 * Version 0.3: 2008-02-07 Added onDragStart option
69 * Made the scroll amount configurable (default is 5 as before)
70 * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
71 * Added onAllowDrop to control dropping
72 * Fixed a bug which meant that you couldn't set the scroll amount in both directions
73 * Added serialize method
74 * Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
76 * Improved the serialize method to use a default (and settable) regular expression.
77 * Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
78 * Version 0.6: 2011-12-02 Added support for touch devices
79 * Version 0.7 2012-04-09 Now works with jQuery 1.7 and supports touch, tidied up tabs and spaces
81 !function ($, window, document, undefined) {
82 // Determine if this is a touch device
83 var hasTouch = 'ontouchstart' in document.documentElement,
84 startEvent = hasTouch ? 'touchstart' : 'mousedown',
85 moveEvent = hasTouch ? 'touchmove' : 'mousemove',
86 endEvent = hasTouch ? 'touchend' : 'mouseup';
88 // If we're on a touch device, then wire up the events
89 // see http://stackoverflow.com/a/8456194/1316086
91 && $.each("touchstart touchmove touchend".split(" "), function(i, name) {
92 $.event.fixHooks[name] = $.event.mouseHooks;
96 $(document).ready(function () {
97 function parseStyle(css) {
99 parts = css.match(/([^;:]+)/g) || [];
101 objMap[parts.shift()] = parts.shift().trim();
105 $('table').each(function () {
106 if ($(this).data('table') == 'dnd') {
109 onDragStyle: $(this).data('ondragstyle') && parseStyle($(this).data('ondragstyle')) || null,
110 onDropStyle: $(this).data('ondropstyle') && parseStyle($(this).data('ondropstyle')) || null,
111 onDragClass: $(this).data('ondragclass') == undefined && "tDnD_whileDrag" || $(this).data('ondragclass'),
112 onDrop: $(this).data('ondrop') && new Function('table', 'row', $(this).data('ondrop')), // 'return eval("'+$(this).data('ondrop')+'");') || null,
113 onDragStart: $(this).data('ondragstart') && new Function('table', 'row' ,$(this).data('ondragstart')), // 'return eval("'+$(this).data('ondragstart')+'");') || null,
114 scrollAmount: $(this).data('scrollamount') || 5,
115 sensitivity: $(this).data('sensitivity') || 10,
116 hierarchyLevel: $(this).data('hierarchylevel') || 0,
117 indentArtifact: $(this).data('indentartifact') || '<div class="indent"> </div>',
118 autoWidthAdjust: $(this).data('autowidthadjust') || true,
119 autoCleanRelations: $(this).data('autocleanrelations') || true,
120 jsonPretifySeparator: $(this).data('jsonpretifyseparator') || '\t',
121 serializeRegexp: $(this).data('serializeregexp') && new RegExp($(this).data('serializeregexp')) || /[^\-]*$/,
122 serializeParamName: $(this).data('serializeparamname') || false,
123 dragHandle: $(this).data('draghandle') || null
131 window.jQuery.tableDnD = {
132 /** Keep hold of the current table being dragged */
134 /** Keep hold of the current drag object if any */
136 /** The current mouse offset */
138 /** Remember the old value of X and Y so that we don't do too much processing */
142 /** Actually build the structure */
143 build: function(options) {
144 // Set up the defaults if any
146 this.each(function() {
147 // This is bound to each matching table, set up the defaults and override with user options
148 this.tableDnDConfig = $.extend({
151 // Add in the default class for whileDragging
152 onDragClass: "tDnD_whileDrag",
156 /** Sensitivity setting will throttle the trigger rate for movement detection */
158 /** Hierarchy level to support parent child. 0 switches this functionality off */
160 /** The html artifact to prepend the first cell with as indentation */
161 indentArtifact: '<div class="indent"> </div>',
162 /** Automatically adjust width of first cell */
163 autoWidthAdjust: true,
164 /** Automatic clean-up to ensure relationship integrity */
165 autoCleanRelations: true,
166 /** Specify a number (4) as number of spaces or any indent string for JSON.stringify */
167 jsonPretifySeparator: '\t',
168 /** The regular expression to use to trim row IDs */
169 serializeRegexp: /[^\-]*$/,
170 /** If you want to specify another parameter name instead of the table ID */
171 serializeParamName: false,
172 /** If you give the name of a class here, then only Cells with this class will be draggable */
176 // Now make the rows draggable
177 $.tableDnD.makeDraggable(this);
178 // Prepare hierarchy support
179 this.tableDnDConfig.hierarchyLevel
180 && $.tableDnD.makeIndented(this);
183 // Don't break the chain
186 makeIndented: function (table) {
187 var config = table.tableDnDConfig,
189 firstCell = $(rows).first().find('td:first')[0],
195 if ($(table).hasClass('indtd'))
198 tableStyle = $(table).addClass('indtd').attr('style');
199 $(table).css({whiteSpace: "nowrap"});
201 for (var w = 0; w < rows.length; w++) {
202 if (cellWidth < $(rows[w]).find('td:first').text().length) {
203 cellWidth = $(rows[w]).find('td:first').text().length;
207 $(firstCell).css({width: 'auto'});
208 for (w = 0; w < config.hierarchyLevel; w++)
209 $(rows[longestCell]).find('td:first').prepend(config.indentArtifact);
210 firstCell && $(firstCell).css({width: firstCell.offsetWidth});
211 tableStyle && $(table).css(tableStyle);
213 for (w = 0; w < config.hierarchyLevel; w++)
214 $(rows[longestCell]).find('td:first').children(':first').remove();
216 config.hierarchyLevel
217 && $(rows).each(function () {
218 indentLevel = $(this).data('level') || 0;
219 indentLevel <= config.hierarchyLevel
220 && $(this).data('level', indentLevel)
221 || $(this).data('level', 0);
222 for (var i = 0; i < $(this).data('level'); i++)
223 $(this).find('td:first').prepend(config.indentArtifact);
228 /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
229 makeDraggable: function(table) {
230 var config = table.tableDnDConfig;
233 // We only need to add the event to the specified cells
234 && $(config.dragHandle, table).each(function() {
235 // The cell is bound to "this"
236 $(this).bind(startEvent, function(e) {
237 $.tableDnD.initialiseDrag($(this).parents('tr')[0], table, this, e, config);
241 // For backwards compatibility, we add the event to the whole row
242 // get all the rows as a wrapped set
243 || $(table.rows).each(function() {
244 // Iterate through each row, the row is bound to "this"
245 if (! $(this).hasClass("nodrag")) {
246 $(this).bind(startEvent, function(e) {
247 if (e.target.tagName == "TD") {
248 $.tableDnD.initialiseDrag(this, table, this, e, config);
251 }).css("cursor", "move"); // Store the tableDnD object
255 currentOrder: function() {
256 var rows = this.currentTable.rows;
257 return $.map(rows, function (val) {
258 return ($(val).data('level') + val.id).replace(/\s/g, '');
261 initialiseDrag: function(dragObject, table, target, e, config) {
262 this.dragObject = dragObject;
263 this.currentTable = table;
264 this.mouseOffset = this.getMouseOffset(target, e);
265 this.originalOrder = this.currentOrder();
267 // Now we need to capture the mouse up and mouse move event
268 // We can use bind so that we don't interfere with other event handlers
270 .bind(moveEvent, this.mousemove)
271 .bind(endEvent, this.mouseup);
273 // Call the onDragStart method if there is one
275 && config.onDragStart(table, target);
277 updateTables: function() {
278 this.each(function() {
279 // this is now bound to each matching table
280 if (this.tableDnDConfig)
281 $.tableDnD.makeDraggable(this);
284 /** Get the mouse coordinates from the event (allowing for browser differences) */
285 mouseCoords: function(e) {
286 if(e.pageX || e.pageY)
293 x: e.clientX + document.body.scrollLeft - document.body.clientLeft,
294 y: e.clientY + document.body.scrollTop - document.body.clientTop
297 /** Given a target element and a mouse eent, get the mouse offset from that element.
298 To do this we need the element's position and the mouse position */
299 getMouseOffset: function(target, e) {
303 e = e || window.event;
305 docPos = this.getPosition(target);
306 mousePos = this.mouseCoords(e);
309 x: mousePos.x - docPos.x,
310 y: mousePos.y - docPos.y
313 /** Get the position of an element by going up the DOM tree and adding up all the offsets */
314 getPosition: function(element) {
318 // Safari fix -- thanks to Luis Chato for this!
319 // Safari 2 doesn't correctly grab the offsetTop of a table row
320 // this is detailed here:
321 // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
322 // the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
323 // note that firefox will return a text node as a first child, so designing a more thorough
324 // solution may need to take that into account, for now this seems to work in firefox, safari, ie
325 if (element.offsetHeight == 0)
326 element = element.firstChild; // a table cell
328 while (element.offsetParent) {
329 left += element.offsetLeft;
330 top += element.offsetTop;
331 element = element.offsetParent;
334 left += element.offsetLeft;
335 top += element.offsetTop;
342 autoScroll: function (mousePos) {
343 var config = this.currentTable.tableDnDConfig,
344 yOffset = window.pageYOffset,
345 windowHeight = window.innerHeight
347 : document.documentElement.clientHeight
348 ? document.documentElement.clientHeight
349 : document.body.clientHeight;
352 // yOffset=document.body.scrollTop;
354 if (typeof document.compatMode != 'undefined'
355 && document.compatMode != 'BackCompat')
356 yOffset = document.documentElement.scrollTop;
357 else if (typeof document.body != 'undefined')
358 yOffset = document.body.scrollTop;
360 mousePos.y - yOffset < config.scrollAmount
361 && window.scrollBy(0, - config.scrollAmount)
362 || windowHeight - (mousePos.y - yOffset) < config.scrollAmount
363 && window.scrollBy(0, config.scrollAmount);
366 moveVerticle: function (moving, currentRow) {
368 if (0 != moving.vertical
369 // If we're over a row then move the dragged row to there so that the user sees the
370 // effect dynamically
372 && this.dragObject != currentRow
373 && this.dragObject.parentNode == currentRow.parentNode)
375 && this.dragObject.parentNode.insertBefore(this.dragObject, currentRow.nextSibling)
376 || 0 < moving.vertical
377 && this.dragObject.parentNode.insertBefore(this.dragObject, currentRow);
380 moveHorizontal: function (moving, currentRow) {
381 var config = this.currentTable.tableDnDConfig,
384 if (!config.hierarchyLevel
385 || 0 == moving.horizontal
386 // We only care if moving left or right on the current row
388 || this.dragObject != currentRow)
391 currentLevel = $(currentRow).data('level');
393 0 < moving.horizontal
395 && $(currentRow).find('td:first').children(':first').remove()
396 && $(currentRow).data('level', --currentLevel);
398 0 > moving.horizontal
399 && currentLevel < config.hierarchyLevel
400 && $(currentRow).prev().data('level') >= currentLevel
401 && $(currentRow).children(':first').prepend(config.indentArtifact)
402 && $(currentRow).data('level', ++currentLevel);
405 mousemove: function(e) {
406 var dragObj = $($.tableDnD.dragObject),
407 config = $.tableDnD.currentTable.tableDnDConfig,
414 e && e.preventDefault();
416 if (!$.tableDnD.dragObject)
419 // prevent touch device screen scrolling
420 e.type == 'touchmove'
421 && event.preventDefault(); // TODO verify this is event and not really e
423 // update the style to show we're dragging
425 && dragObj.addClass(config.onDragClass)
426 || dragObj.css(config.onDragStyle);
428 mousePos = $.tableDnD.mouseCoords(e);
429 x = mousePos.x - $.tableDnD.mouseOffset.x;
430 y = mousePos.y - $.tableDnD.mouseOffset.y;
432 // auto scroll the window
433 $.tableDnD.autoScroll(mousePos);
435 currentRow = $.tableDnD.findDropTargetRow(dragObj, y);
436 moving = $.tableDnD.findDragDirection(x, y);
438 $.tableDnD.moveVerticle(moving, currentRow);
439 $.tableDnD.moveHorizontal(moving, currentRow);
443 findDragDirection: function (x,y) {
444 var sensitivity = this.currentTable.tableDnDConfig.sensitivity,
447 xMin = oldX - sensitivity,
448 xMax = oldX + sensitivity,
449 yMin = oldY - sensitivity,
450 yMax = oldY + sensitivity,
452 horizontal: x >= xMin && x <= xMax ? 0 : x > oldX ? -1 : 1,
453 vertical : y >= yMin && y <= yMax ? 0 : y > oldY ? -1 : 1
456 // update the old value
457 if (moving.horizontal != 0)
459 if (moving.vertical != 0)
464 /** We're only worried about the y position really, because we can only move rows up and down */
465 findDropTargetRow: function(draggedRow, y) {
467 rows = this.currentTable.rows,
468 config = this.currentTable.tableDnDConfig,
472 for (var i = 0; i < rows.length; i++) {
474 rowY = this.getPosition(row).y;
475 rowHeight = parseInt(row.offsetHeight) / 2;
476 if (row.offsetHeight == 0) {
477 rowY = this.getPosition(row.firstChild).y;
478 rowHeight = parseInt(row.firstChild.offsetHeight) / 2;
480 // Because we always have to insert before, we need to offset the height a bit
481 if (y > (rowY - rowHeight) && y < (rowY + rowHeight))
482 // that's the row we're over
483 // If it's the same as the current row, ignore it
484 if (row == draggedRow
485 || (config.onAllowDrop
486 && !config.onAllowDrop(draggedRow, row))
487 // If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
488 || $(row).hasClass("nodrop"))
495 processMouseup: function() {
496 var config = this.currentTable.tableDnDConfig,
497 droppedRow = this.dragObject,
501 if (!this.currentTable || !droppedRow)
504 // Unbind the event handlers
506 .unbind(moveEvent, this.mousemove)
507 .unbind(endEvent, this.mouseup);
509 config.hierarchyLevel
510 && config.autoCleanRelations
511 && $(this.currentTable.rows).first().find('td:first').children().each(function () {
512 myLevel = $(this).parents('tr:first').data('level');
514 && $(this).parents('tr:first').data('level', --myLevel)
517 && config.hierarchyLevel > 1
518 && $(this.currentTable.rows).each(function () {
519 myLevel = $(this).data('level');
521 parentLevel = $(this).prev().data('level');
522 while (myLevel > parentLevel + 1) {
523 $(this).find('td:first').children(':first').remove();
524 $(this).data('level', --myLevel);
529 // If we have a dragObject, then we need to release it,
530 // The row will already have been moved to the right place so we just reset stuff
532 && $(droppedRow).removeClass(config.onDragClass)
533 || $(droppedRow).css(config.onDropStyle);
535 this.dragObject = null;
536 // Call the onDrop method if there is one
538 && this.originalOrder != this.currentOrder()
539 && $(droppedRow).hide().fadeIn('fast')
540 && config.onDrop(this.currentTable, droppedRow);
542 this.currentTable = null; // let go of the table too
544 mouseup: function(e) {
545 e && e.preventDefault();
546 $.tableDnD.processMouseup();
549 jsonize: function(pretify) {
550 var table = this.currentTable;
552 return JSON.stringify(
553 this.tableData(table),
555 table.tableDnDConfig.jsonPretifySeparator
557 return JSON.stringify(this.tableData(table));
559 serialize: function() {
560 return $.param(this.tableData(this.currentTable));
562 serializeTable: function(table) {
564 var paramName = table.tableDnDConfig.serializeParamName || table.id;
565 var rows = table.rows;
566 for (var i=0; i<rows.length; i++) {
567 if (result.length > 0) result += "&";
568 var rowId = rows[i].id;
569 if (rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
570 rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
571 result += paramName + '[]=' + rowId;
576 serializeTables: function() {
578 $('table').each(function() {
579 this.id && result.push($.param(this.tableData(this)));
581 return result.join('&');
583 tableData: function (table) {
584 var config = table.tableDnDConfig,
596 table = this.currentTable;
597 if (!table || !table.id || !table.rows || !table.rows.length)
598 return {error: { code: 500, message: "Not a valid table, no serializable unique id provided."}};
600 rows = config.autoCleanRelations
602 || $.makeArray(table.rows);
603 paramName = config.serializeParamName || table.id;
604 currentID = paramName;
606 getSerializeRegexp = function (rowId) {
607 if (rowId && config && config.serializeRegexp)
608 return rowId.match(config.serializeRegexp)[0];
612 data[currentID] = [];
613 !config.autoCleanRelations
614 && $(rows[0]).data('level')
615 && rows.unshift({id: 'undefined'});
619 for (var i=0; i < rows.length; i++) {
620 if (config.hierarchyLevel) {
621 indentLevel = $(rows[i]).data('level') || 0;
622 if (indentLevel == 0) {
623 currentID = paramName;
626 else if (indentLevel > currentLevel) {
627 previousIDs.push([currentID, currentLevel]);
628 currentID = getSerializeRegexp(rows[i-1].id);
630 else if (indentLevel < currentLevel) {
631 for (var h = 0; h < previousIDs.length; h++) {
632 if (previousIDs[h][1] == indentLevel)
633 currentID = previousIDs[h][0];
634 if (previousIDs[h][1] >= currentLevel)
635 previousIDs[h][1] = 0;
638 currentLevel = indentLevel;
640 if (!$.isArray(data[currentID]))
641 data[currentID] = [];
642 rowID = getSerializeRegexp(rows[i].id);
643 rowID && data[currentID].push(rowID);
646 rowID = getSerializeRegexp(rows[i].id);
647 rowID && data[currentID].push(rowID);
654 window.jQuery.fn.extend(
656 tableDnD : $.tableDnD.build,
657 tableDnDUpdate : $.tableDnD.updateTables,
658 tableDnDSerialize : $.proxy($.tableDnD.serialize, $.tableDnD),
659 tableDnDSerializeAll : $.tableDnD.serializeTables,
660 tableDnDData : $.proxy($.tableDnD.tableData, $.tableDnD)
664 }(window.jQuery, window, window.document);