// $Id $ Drupal.behaviors.tableDrag = function(context) { for (var base in Drupal.settings.tabledrag) { if (!$('#' + base + '.tabledrag-processed').size(), context) { var fieldSettings = Drupal.settings.tabledrag[base]; $('#' + base + ':not(.tabledrag-processed)').each(function() { // Create the new tabledrag instance. Save in the Drupal variable // to allow other scripts access to the object. Drupal.tabledrag[base] = new Drupal.tabledrag(fieldSettings); Drupal.tabledrag[base].init(this); }); $('#' + base).addClass('tabledrag-processed'); } } } Drupal.tabledrag = function(fieldSettings) { this.fieldSettings = fieldSettings; this.dragObject = null; // Keep hold of the current drag object if any. this.table = null; // The current table. this.oldY = 0; // Remember the old value of Y so that we don't do too much processing. } // Initialize the drag and drop by capturing mouse move events. Drupal.tabledrag.prototype.init = function(table) { var self = this; this.table = table; // Check if this table contains indentations. var indents = $('div.indentation', table); this.indentEnabled = indents.size() > 0 ? true : false; if (this.indentEnabled) { var indentSize = indents.css('width'); this.oldX = 0; this.indentCount = 1; // Total width of indents, set in makeDraggable. this.indentIncrement = indentSize.replace(/[0-9\.]*/, ''); this.indentAmount = parseInt(indentSize); } // Make each applicable row draggable. $('tr.draggable', table).each(function() { self.makeDraggable(this); }); // Hide columns containing affected form elements. this.hideColumns(); // Now make the onmousemove method in the context of "self" so that we can get back to tableDnD $(document).bind('mousemove', { tabledrag: self }, self.dragRow); $(document).bind('mouseup', { tabledrag: self }, self.dropRow); } /** * Hide the columns containing form elements according to the settings for * this tabledrag instance. */ Drupal.tabledrag.prototype.hideColumns = function() { for (group in this.fieldSettings) { var hidden = this.fieldSettings[group][0]['hidden']; var field = $('.' + this.fieldSettings[group][0]['target'] + ':first', this.table); var cell = field.parents('td:first, th:first'); if (hidden && cell[0] && cell.css('display') != 'none') { var columnIndex = $('td, th', field.parents('tr:first')).index(cell.get(0)); columnIndex++; // nth-child selector is 1 based, not 0 based. $('tr', this.table).each(function() { // Find and hide the cell in the table body. var cell = $('td:nth-child('+ columnIndex +')', this); if (cell.size()) { cell.css('display', 'none'); } // We might be dealing with a row spanning the entire table. // Reduce the colspan on the first cell to prevent the cell from // overshooting the table. else { cell = $('td:first', this); cell.attr('colspan', cell.attr('colspan') - 1); } // Remove table header cells entirely (Safari doesn't hide properly). var th = $('th:nth-child('+ columnIndex +')', this); if (th.size()) { th.remove(); } }); } } } /** * Find the target used within a particular row and group. */ Drupal.tabledrag.prototype.rowSettings = function(group, row) { var field = $('.' + group, row); for (delta in this.fieldSettings[group]) { var targetClass = this.fieldSettings[group][delta]['target']; if (field.is('.' + targetClass)) { return this.fieldSettings[group][delta]; } } }; /** * Take an item and add an onmousedown method so that we can make it draggable. */ Drupal.tabledrag.prototype.makeDraggable = function(item) { if (!item) return; var self = this; // Keep the context of the tabledrag object. // Create the handle. handle = $('
'); // Insert after indentations (if any). var indentAdded = $('td:first .indentation:last', item).after(handle).size(); if (!indentAdded) { $('td:first', item).prepend(handle); } else { self.indentCount = Math.max($('.indentation', item).size(), self.indentCount); } // Add hover action for the handle. handle.hover(function() { self.dragObject == null ? $(this).addClass('tabledrag-handle-hover') : null; }, function() { self.dragObject == null ? $(this).removeClass('tabledrag-handle-hover') : null; }); // Add the mousedown action for the handle. handle.mousedown(function(event) { self.dragObject = new Object(); self.dragObject.element = item; self.dragObject.initMouseOffset = self.getMouseOffset(item, event); self.dragObject.initMousePos = self.mouseCoords(event); self.dragObject.indentMousePos = self.dragObject.initMousePos; self.dragObject.indents = $('.indentation', item).size(); $(this).addClass('tabledrag-handle-hover'); // Update the style to show we're dragging. $(this).parents('tr:first').addClass('highlight'); return false; }); } /** * Mousemove event handler, bound to document. */ Drupal.tabledrag.prototype.dragRow = function(event) { var self = event.data.tabledrag; if (self.dragObject) { var mousePos = self.mouseCoords(event); // Handle swapping rows. var y = mousePos.y - self.dragObject.initMouseOffset.y; if (y != self.oldY) { // Going up or down... var movingDown = y > self.oldY; // Update the old value. self.oldY = y; // Check if we need to move any rows. var currentRow = self.findDropTargetRow(y); if (currentRow && self.dragObject.element != currentRow) { if (movingDown) { $(currentRow).after(self.dragObject.element); } else if (!movingDown) { $(currentRow).before(self.dragObject.element); } self.restripeTable(self.table); } } // Similar to row swapping, handle indentations. var x = mousePos.x - self.dragObject.initMouseOffset.x; if (self.indentEnabled && x != self.oldX) { self.oldX = x; var xDiff = mousePos.x - self.dragObject.indentMousePos.x; var indentDiff = parseInt(xDiff / self.indentAmount); // Limit the indentation to no less than 0 and no more than // the total amount of indentation in the table. indentDiff = indentDiff > 0 ? Math.min(indentDiff, self.indentCount - self.dragObject.indents + 1) : Math.max(indentDiff, -self.dragObject.indents); if (indentDiff != 0) { // Further limit the diff if (indentDiff > 0) { var prevIndent = $('.indentation', $(self.dragObject.element).prev()).size(); indentDiff = Math.min(prevIndent - self.dragObject.indents + 1, indentDiff); } for (var n = 1; n <= Math.abs(indentDiff); n++) { // Add or remove indentations. if (indentDiff < 0) { $('.indentation:first', self.dragObject.element).remove(); self.dragObject.indents--; } else { $('td:first', self.dragObject.element).prepend(Drupal.theme('tabledragIndentation')); self.dragObject.indents++; } } // Update indentation for this actual row. self.dragObject.indentMousePos.x += self.indentAmount * indentDiff; // Update table total indentations. self.indentCount = Math.max(self.indentCount, self.dragObject.indents); } } return false; } }; /** * Mouseup event handler, bound to document. */ Drupal.tabledrag.prototype.dropRow = function(event) { var self = event.data.tabledrag; if (self.dragObject != null) { var droppedRow = self.dragObject.element; // The row is already in the right place so we just release it. $(droppedRow).removeClass('highlight'); $('.tabledrag-handle', droppedRow).removeClass('tabledrag-handle-hover'); self.updateFields(droppedRow); self.markChanged(droppedRow); self.dragObject = null; self.onDrop(droppedRow); } }; /** * Stub function. Allows custom javascript to add a handler when the row is * dropped. */ Drupal.tabledrag.prototype.onDrop = function(droppedRow) { return null; } /** * Get the position of an element by going up the DOM tree and adding up all * the offsets. */ Drupal.tabledrag.prototype.getPosition = function(event){ var left = 0; var top = 0; // Safari fix -- thanks to Luis Chato for this! if (event.offsetHeight == 0) { /* Safari 2 doesn't correctly grab the offsetTop of a table row this is detailed here: http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/ the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild. note that firefox will return a text node as a first child, so designing a more thorough solution may need to take that into account, this seems to work in Firefox, Safari, and IE. */ event = event.firstChild; // a table cell } while (event.offsetParent){ left += event.offsetLeft; top += event.offsetTop; event = event.offsetParent; } left += event.offsetLeft; top += event.offsetTop; return {x:left, y:top}; } /** * Get the mouse coordinates from the event (allowing for browser differences). */ Drupal.tabledrag.prototype.mouseCoords = function(event){ if (event.pageX || event.pageY) { return {x:event.pageX, y:event.pageY}; } return { x:event.clientX + document.body.scrollLeft - document.body.clientLeft, y:event.clientY + document.body.scrollTop - document.body.clientTop }; } /** * Given a target element and a mouse event, get the mouse offset from that * element. To do this we need the element's position and the mouse position. */ Drupal.tabledrag.prototype.getMouseOffset = function(target, event){ event = event || window.event; var docPos = this.getPosition(target); var mousePos = this.mouseCoords(event); return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y}; } /** * Find the row the mouse is currently over. This row is then taken and swapped * with the one being dragged. */ Drupal.tabledrag.prototype.findDropTargetRow = function(y) { var rows = this.table.tBodies[0].rows; for (var n=0; n (rowY - rowHeight)) && (y < (rowY + rowHeight))) { // We've found the row the mouse just passed over, but it doesn't take // into account hidden rows. Skip backwards until we find a draggable // row. while ($(row).is(':not(.draggable)') && $(row).prev().is(':not(.draggable)')) { row = $(row).prev().get(0); } // Special case for the first row, if it's not draggable, do not allow // another row to be put before it. if (!(n == 0 && $(row).is(':not(.draggable)'))) { return row; } } }; return null; } /** * After the row is dropped, update the fields inside of it according to the * dragtable settings. */ Drupal.tabledrag.prototype.updateFields = function() { changedRow = this.dragObject.element; for (group in this.fieldSettings) { // Each group may have a different setting for relationship, so we find // the source rows for each seperately. var rowSettings = this.rowSettings(group, changedRow); // Siblings are easy, check previous and next rows. if (rowSettings.relationship == 'sibling') { var previousRow = $(changedRow).prev().get(0); var nextRow = $(changedRow).next().get(0); var sourceRow = changedRow; if ($(previousRow).is('.draggable')) { sourceRow = previousRow; } else if ($(nextRow).is('.draggable')) { sourceRow = nextRow; } } // Parents, look up the tree until we find a field not in this group. // Go up as many parents as indentations in the changed row. else if (rowSettings.relationship == 'parent') { var parents = this.indentCount - this.dragObject.indents; var previousRow = $(changedRow).prev(); while (parents > 0) { parents--; while (previousRow.length && $('.indentation', previousRow).length < (this.dragObject.indents - parents)) { previousRow = previousRow.prev(); } } sourceRow = previousRow[0] || changedRow; } // Because we may have moved the row from one category to another, // take a look at our sibling and borrow its sources and targets. this.copyDragClasses(sourceRow, changedRow, group); for (delta in this.fieldSettings[group]) { var targetClass = '.' + this.fieldSettings[group][delta]['target']; var targetElement = $(targetClass, changedRow).get(0); // Check if a target element exists in this row. if (targetElement) { var relationship = this.fieldSettings[group][delta]['relationship']; var action = this.fieldSettings[group][delta]['action']; var sourceClass = '.' + this.fieldSettings[group][delta]['source']; var sourceElement = $(sourceClass, sourceRow).get(0); switch (action) { case 'match': // Update the value. targetElement.value = sourceElement.value; break; case 'order': if ($(targetElement).is('select')) { // Get a list of acceptable values. var values = new Array(); $('option', targetElement).each(function() { values.push(this.value); }); $(targetClass, this.table).each(function() { this.value = values.shift(); }); } else { // Assume a numeric input field. var weight = 0; $(targetClass, this.table).each(function() { this.value = weight; weight++; }); } break; } } } } } /** * Copy all special tabledrag classes from one row's form elements to a * different one, removing any special classes that the destination row * may have had. */ Drupal.tabledrag.prototype.copyDragClasses = function(sourceRow, targetRow, group) { var sourceElement = $('.' + group, sourceRow); var targetElement = $('.' + group, targetRow); if (sourceElement.length && targetElement.length) { targetElement[0].className = sourceElement[0].className; } } /** * Add an asterisk or other marker to the changed row. */ Drupal.tabledrag.prototype.markChanged = function(changedRow) { var marker = $('
' + Drupal.theme('tabledragChangedMarker') + '
'); var cell = $('td:first', changedRow); // Converting marker to a DOM object and using .html() to do string // comparision ensures consitency across browsers. if (cell.html().indexOf(marker.html()) == -1) { cell.append(marker.html()); } } Drupal.tabledrag.prototype.restripeTable = function(table) { // :even and :odd are reversed because jquery counts from 0 and // we count from 1, so we're out of sync. $('tbody tr:not(:hidden)', table) .filter(':odd').filter('.odd') .removeClass('odd').addClass('even') .end().end() .filter(':even').filter('.even') .removeClass('even').addClass('odd'); } Drupal.theme.prototype.tabledragChangedMarker = function () { return '*'; }; Drupal.theme.prototype.tabledragIndentation = function () { return '
 
'; };