// $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 '
';
};