4 * Drag and drop table rows with field manipulation.
6 * Using the drupal_add_tabledrag() function, any table with weights or parent
7 * relationships may be made into draggable tables. Columns containing a field
8 * may optionally be hidden, providing a better user experience.
10 * Created tableDrag instances may be modified with custom behaviors by
11 * overriding the .onDrag, .onDrop, .row.onSwap, and .row.onIndent methods.
12 * See blocks.js for an example of adding additional functionality to tableDrag.
14 Drupal
.behaviors
.tableDrag
= {
15 attach: function (context
, settings
) {
16 for (var base
in settings
.tableDrag
) {
17 $('#' + base
, context
).once('tabledrag', function () {
18 // Create the new tableDrag instance. Save in the Drupal variable
19 // to allow other scripts access to the object.
20 Drupal
.tableDrag
[base
] = new Drupal
.tableDrag(this, settings
.tableDrag
[base
]);
27 * Constructor for the tableDrag object. Provides table and field manipulation.
30 * DOM object for the table to be made draggable.
31 * @param tableSettings
32 * Settings for the table added via drupal_add_dragtable().
34 Drupal
.tableDrag = function (table
, tableSettings
) {
37 // Required object variables.
39 this.tableSettings
= tableSettings
;
40 this.dragObject
= null; // Used to hold information about a current drag operation.
41 this.rowObject
= null; // Provides operations for row manipulation.
42 this.oldRowElement
= null; // Remember the previous element.
43 this.oldY
= 0; // Used to determine up or down direction from last mouse move.
44 this.changed
= false; // Whether anything in the entire table has changed.
45 this.maxDepth
= 0; // Maximum amount of allowed parenting.
46 this.rtl
= $(this.table
).css('direction') == 'rtl' ? -1 : 1; // Direction of the table.
48 // Configure the scroll settings.
49 this.scrollSettings
= { amount
: 4, interval
: 50, trigger
: 70 };
50 this.scrollInterval
= null;
52 this.windowHeight
= 0;
54 // Check this table's settings to see if there are parent relationships in
55 // this table. For efficiency, large sections of code can be skipped if we
56 // don't need to track horizontal movement and indentations.
57 this.indentEnabled
= false;
58 for (var group
in tableSettings
) {
59 for (var n
in tableSettings
[group
]) {
60 if (tableSettings
[group
][n
].relationship
== 'parent') {
61 this.indentEnabled
= true;
63 if (tableSettings
[group
][n
].limit
> 0) {
64 this.maxDepth
= tableSettings
[group
][n
].limit
;
68 if (this.indentEnabled
) {
69 this.indentCount
= 1; // Total width of indents, set in makeDraggable.
70 // Find the width of indentations to measure mouse movements against.
71 // Because the table doesn't need to start with any indentations, we
72 // manually append 2 indentations in the first draggable row, measure
73 // the offset, then remove.
74 var indent
= Drupal
.theme('tableDragIndentation');
75 var testRow
= $('<tr/>').addClass('draggable').appendTo(table
);
76 var testCell
= $('<td/>').appendTo(testRow
).prepend(indent
).prepend(indent
);
77 this.indentAmount
= $('.indentation', testCell
).get(1).offsetLeft
- $('.indentation', testCell
).get(0).offsetLeft
;
81 // Make each applicable row draggable.
82 // Match immediate children of the parent element to allow nesting.
83 $('> tr.draggable, > tbody > tr.draggable', table
).each(function () { self
.makeDraggable(this); });
85 // Add a link before the table for users to show or hide weight columns.
86 $(table
).before($('<a href="#" class="tabledrag-toggle-weight"></a>')
87 .attr('title', Drupal
.t('Re-order rows by numerical weight instead of dragging.'))
89 if ($.cookie('Drupal.tableDrag.showWeight') == 1) {
97 .wrap('<div class="tabledrag-toggle-weight-wrapper"></div>')
101 // Initialize the specified columns (for example, weight or parent columns)
102 // to show or hide according to user preference. This aids accessibility
103 // so that, e.g., screen reader users can choose to enter weight values and
104 // manipulate form elements directly, rather than using drag-and-drop..
107 // Add mouse bindings to the document. The self variable is passed along
108 // as event handlers do not have direct access to the tableDrag object.
109 $(document
).bind('mousemove', function (event
) { return self
.dragRow(event
, self
); });
110 $(document
).bind('mouseup', function (event
) { return self
.dropRow(event
, self
); });
114 * Initialize columns containing form elements to be hidden by default,
115 * according to the settings for this tableDrag instance.
117 * Identify and mark each cell with a CSS class so we can easily toggle
118 * show/hide it. Finally, hide columns if user does not have a
119 * 'Drupal.tableDrag.showWeight' cookie.
121 Drupal
.tableDrag
.prototype.initColumns = function () {
122 for (var group
in this.tableSettings
) {
123 // Find the first field in this group.
124 for (var d
in this.tableSettings
[group
]) {
125 var field
= $('.' + this.tableSettings
[group
][d
].target
+ ':first', this.table
);
126 if (field
.size() && this.tableSettings
[group
][d
].hidden
) {
127 var hidden
= this.tableSettings
[group
][d
].hidden
;
128 var cell
= field
.parents('td:first');
133 // Mark the column containing this field so it can be hidden.
134 if (hidden
&& cell
[0]) {
135 // Add 1 to our indexes. The nth-child selector is 1 based, not 0 based.
136 // Match immediate children of the parent element to allow nesting.
137 var columnIndex
= $('> td', cell
.parent()).index(cell
.get(0)) + 1;
138 $('> thead > tr, > tbody > tr, > tr', this.table
).each(function () {
139 // Get the columnIndex and adjust for any colspans in this row.
140 var index
= columnIndex
;
141 var cells
= $(this).children();
142 cells
.each(function (n
) {
143 if (n
< index
&& this.colSpan
&& this.colSpan
> 1) {
144 index
-= this.colSpan
- 1;
148 cell
= cells
.filter(':nth-child(' + index
+ ')');
149 if (cell
[0].colSpan
&& cell
[0].colSpan
> 1) {
150 // If this cell has a colspan, mark it so we can reduce the colspan.
151 cell
.addClass('tabledrag-has-colspan');
154 // Mark this cell so we can hide it.
155 cell
.addClass('tabledrag-hide');
162 // Now hide cells and reduce colspans unless cookie indicates previous choice.
163 // Set a cookie if it is not already present.
164 if ($.cookie('Drupal.tableDrag.showWeight') === null) {
165 $.cookie('Drupal.tableDrag.showWeight', 0, {
166 path
: Drupal
.settings
.basePath
,
167 // The cookie expires in one year.
172 // Check cookie value and show/hide weight columns accordingly.
174 if ($.cookie('Drupal.tableDrag.showWeight') == 1) {
184 * Hide the columns containing weight/parent form elements.
185 * Undo showColumns().
187 Drupal
.tableDrag
.prototype.hideColumns = function () {
188 // Hide weight/parent cells and headers.
189 $('.tabledrag-hide', 'table.tabledrag-processed').css('display', 'none');
190 // Show TableDrag handles.
191 $('.tabledrag-handle', 'table.tabledrag-processed').css('display', '');
192 // Reduce the colspan of any effected multi-span columns.
193 $('.tabledrag-has-colspan', 'table.tabledrag-processed').each(function () {
194 this.colSpan
= this.colSpan
- 1;
197 $('.tabledrag-toggle-weight').text(Drupal
.t('Show row weights'));
199 $.cookie('Drupal.tableDrag.showWeight', 0, {
200 path
: Drupal
.settings
.basePath
,
201 // The cookie expires in one year.
207 * Show the columns containing weight/parent form elements
208 * Undo hideColumns().
210 Drupal
.tableDrag
.prototype.showColumns = function () {
211 // Show weight/parent cells and headers.
212 $('.tabledrag-hide', 'table.tabledrag-processed').css('display', '');
213 // Hide TableDrag handles.
214 $('.tabledrag-handle', 'table.tabledrag-processed').css('display', 'none');
215 // Increase the colspan for any columns where it was previously reduced.
216 $('.tabledrag-has-colspan', 'table.tabledrag-processed').each(function () {
217 this.colSpan
= this.colSpan
+ 1;
220 $('.tabledrag-toggle-weight').text(Drupal
.t('Hide row weights'));
222 $.cookie('Drupal.tableDrag.showWeight', 1, {
223 path
: Drupal
.settings
.basePath
,
224 // The cookie expires in one year.
230 * Find the target used within a particular row and group.
232 Drupal
.tableDrag
.prototype.rowSettings = function (group
, row
) {
233 var field
= $('.' + group
, row
);
234 for (var delta
in this.tableSettings
[group
]) {
235 var targetClass
= this.tableSettings
[group
][delta
].target
;
236 if (field
.is('.' + targetClass
)) {
237 // Return a copy of the row settings.
238 var rowSettings
= {};
239 for (var n
in this.tableSettings
[group
][delta
]) {
240 rowSettings
[n
] = this.tableSettings
[group
][delta
][n
];
248 * Take an item and add event handlers to make it become draggable.
250 Drupal
.tableDrag
.prototype.makeDraggable = function (item
) {
253 // Create the handle.
254 var handle
= $('<a href="#" class="tabledrag-handle"><div class="handle"> </div></a>').attr('title', Drupal
.t('Drag to re-order'));
255 // Insert the handle after indentations (if any).
256 if ($('td:first .indentation:last', item
).length
) {
257 $('td:first .indentation:last', item
).after(handle
);
258 // Update the total width of indentation in this entire table.
259 self
.indentCount
= Math
.max($('.indentation', item
).size(), self
.indentCount
);
262 $('td:first', item
).prepend(handle
);
265 // Add hover action for the handle.
266 handle
.hover(function () {
267 self
.dragObject
== null ? $(this).addClass('tabledrag-handle-hover') : null;
269 self
.dragObject
== null ? $(this).removeClass('tabledrag-handle-hover') : null;
272 // Add the mousedown action for the handle.
273 handle
.mousedown(function (event
) {
274 // Create a new dragObject recording the event information.
275 self
.dragObject
= {};
276 self
.dragObject
.initMouseOffset
= self
.getMouseOffset(item
, event
);
277 self
.dragObject
.initMouseCoords
= self
.mouseCoords(event
);
278 if (self
.indentEnabled
) {
279 self
.dragObject
.indentMousePos
= self
.dragObject
.initMouseCoords
;
282 // If there's a lingering row object from the keyboard, remove its focus.
283 if (self
.rowObject
) {
284 $('a.tabledrag-handle', self
.rowObject
.element
).blur();
287 // Create a new rowObject for manipulation of this row.
288 self
.rowObject
= new self
.row(item
, 'mouse', self
.indentEnabled
, self
.maxDepth
, true);
290 // Save the position of the table.
291 self
.table
.topY
= $(self
.table
).offset().top
;
292 self
.table
.bottomY
= self
.table
.topY
+ self
.table
.offsetHeight
;
294 // Add classes to the handle and row.
295 $(this).addClass('tabledrag-handle-hover');
296 $(item
).addClass('drag');
298 // Set the document to use the move cursor during drag.
299 $('body').addClass('drag');
300 if (self
.oldRowElement
) {
301 $(self
.oldRowElement
).removeClass('drag-previous');
304 // Hack for IE6 that flickers uncontrollably if select lists are moved.
305 if (navigator
.userAgent
.indexOf('MSIE 6.') != -1) {
306 $('select', this.table
).css('display', 'none');
309 // Hack for Konqueror, prevent the blur handler from firing.
310 // Konqueror always gives links focus, even after returning false on mousedown.
311 self
.safeBlur
= false;
313 // Call optional placeholder function.
318 // Prevent the anchor tag from jumping us to the top of the page.
319 handle
.click(function () {
323 // Similar to the hover event, add a class when the handle is focused.
324 handle
.focus(function () {
325 $(this).addClass('tabledrag-handle-hover');
326 self
.safeBlur
= true;
329 // Remove the handle class on blur and fire the same function as a mouseup.
330 handle
.blur(function (event
) {
331 $(this).removeClass('tabledrag-handle-hover');
332 if (self
.rowObject
&& self
.safeBlur
) {
333 self
.dropRow(event
, self
);
337 // Add arrow-key support to the handle.
338 handle
.keydown(function (event
) {
339 // If a rowObject doesn't yet exist and this isn't the tab key.
340 if (event
.keyCode
!= 9 && !self
.rowObject
) {
341 self
.rowObject
= new self
.row(item
, 'keyboard', self
.indentEnabled
, self
.maxDepth
, true);
344 var keyChange
= false;
345 switch (event
.keyCode
) {
346 case 37: // Left arrow.
347 case 63234: // Safari left arrow.
349 self
.rowObject
.indent(-1 * self
.rtl
);
351 case 38: // Up arrow.
352 case 63232: // Safari up arrow.
353 var previousRow
= $(self
.rowObject
.element
).prev('tr').get(0);
354 while (previousRow
&& $(previousRow
).is(':hidden')) {
355 previousRow
= $(previousRow
).prev('tr').get(0);
358 self
.safeBlur
= false; // Do not allow the onBlur cleanup.
359 self
.rowObject
.direction
= 'up';
362 if ($(item
).is('.tabledrag-root')) {
363 // Swap with the previous top-level row.
365 while (previousRow
&& $('.indentation', previousRow
).size()) {
366 previousRow
= $(previousRow
).prev('tr').get(0);
367 groupHeight
+= $(previousRow
).is(':hidden') ? 0 : previousRow
.offsetHeight
;
370 self
.rowObject
.swap('before', previousRow
);
371 // No need to check for indentation, 0 is the only valid one.
372 window
.scrollBy(0, -groupHeight
);
375 else if (self
.table
.tBodies
[0].rows
[0] != previousRow
|| $(previousRow
).is('.draggable')) {
376 // Swap with the previous row (unless previous row is the first one
378 self
.rowObject
.swap('before', previousRow
);
379 self
.rowObject
.interval
= null;
380 self
.rowObject
.indent(0);
381 window
.scrollBy(0, -parseInt(item
.offsetHeight
, 10));
383 handle
.get(0).focus(); // Regain focus after the DOM manipulation.
386 case 39: // Right arrow.
387 case 63235: // Safari right arrow.
389 self
.rowObject
.indent(1 * self
.rtl
);
391 case 40: // Down arrow.
392 case 63233: // Safari down arrow.
393 var nextRow
= $(self
.rowObject
.group
).filter(':last').next('tr').get(0);
394 while (nextRow
&& $(nextRow
).is(':hidden')) {
395 nextRow
= $(nextRow
).next('tr').get(0);
398 self
.safeBlur
= false; // Do not allow the onBlur cleanup.
399 self
.rowObject
.direction
= 'down';
402 if ($(item
).is('.tabledrag-root')) {
403 // Swap with the next group (necessarily a top-level one).
405 nextGroup
= new self
.row(nextRow
, 'keyboard', self
.indentEnabled
, self
.maxDepth
, false);
407 $(nextGroup
.group
).each(function () {
408 groupHeight
+= $(this).is(':hidden') ? 0 : this.offsetHeight
;
410 nextGroupRow
= $(nextGroup
.group
).filter(':last').get(0);
411 self
.rowObject
.swap('after', nextGroupRow
);
412 // No need to check for indentation, 0 is the only valid one.
413 window
.scrollBy(0, parseInt(groupHeight
, 10));
417 // Swap with the next row.
418 self
.rowObject
.swap('after', nextRow
);
419 self
.rowObject
.interval
= null;
420 self
.rowObject
.indent(0);
421 window
.scrollBy(0, parseInt(item
.offsetHeight
, 10));
423 handle
.get(0).focus(); // Regain focus after the DOM manipulation.
428 if (self
.rowObject
&& self
.rowObject
.changed
== true) {
429 $(item
).addClass('drag');
430 if (self
.oldRowElement
) {
431 $(self
.oldRowElement
).removeClass('drag-previous');
433 self
.oldRowElement
= item
;
434 self
.restripeTable();
438 // Returning false if we have an arrow key to prevent scrolling.
444 // Compatibility addition, return false on keypress to prevent unwanted scrolling.
445 // IE and Safari will suppress scrolling on keydown, but all other browsers
446 // need to return false on keypress. http://www.quirksmode.org/js/keys.html
447 handle
.keypress(function (event
) {
448 switch (event
.keyCode
) {
449 case 37: // Left arrow.
450 case 38: // Up arrow.
451 case 39: // Right arrow.
452 case 40: // Down arrow.
459 * Mousemove event handler, bound to document.
461 Drupal
.tableDrag
.prototype.dragRow = function (event
, self
) {
462 if (self
.dragObject
) {
463 self
.currentMouseCoords
= self
.mouseCoords(event
);
465 var y
= self
.currentMouseCoords
.y
- self
.dragObject
.initMouseOffset
.y
;
466 var x
= self
.currentMouseCoords
.x
- self
.dragObject
.initMouseOffset
.x
;
468 // Check for row swapping and vertical scrolling.
469 if (y
!= self
.oldY
) {
470 self
.rowObject
.direction
= y
> self
.oldY
? 'down' : 'up';
471 self
.oldY
= y
; // Update the old value.
473 // Check if the window should be scrolled (and how fast).
474 var scrollAmount
= self
.checkScroll(self
.currentMouseCoords
.y
);
475 // Stop any current scrolling.
476 clearInterval(self
.scrollInterval
);
477 // Continue scrolling if the mouse has moved in the scroll direction.
478 if (scrollAmount
> 0 && self
.rowObject
.direction
== 'down' || scrollAmount
< 0 && self
.rowObject
.direction
== 'up') {
479 self
.setScroll(scrollAmount
);
482 // If we have a valid target, perform the swap and restripe the table.
483 var currentRow
= self
.findDropTargetRow(x
, y
);
485 if (self
.rowObject
.direction
== 'down') {
486 self
.rowObject
.swap('after', currentRow
, self
);
489 self
.rowObject
.swap('before', currentRow
, self
);
491 self
.restripeTable();
495 // Similar to row swapping, handle indentations.
496 if (self
.indentEnabled
) {
497 var xDiff
= self
.currentMouseCoords
.x
- self
.dragObject
.indentMousePos
.x
;
498 // Set the number of indentations the mouse has been moved left or right.
499 var indentDiff
= Math
.round(xDiff
/ self
.indentAmount
* self
.rtl
);
500 // Indent the row with our estimated diff, which may be further
501 // restricted according to the rows around this row.
502 var indentChange
= self
.rowObject
.indent(indentDiff
);
503 // Update table and mouse indentations.
504 self
.dragObject
.indentMousePos
.x
+= self
.indentAmount
* indentChange
* self
.rtl
;
505 self
.indentCount
= Math
.max(self
.indentCount
, self
.rowObject
.indents
);
513 * Mouseup event handler, bound to document.
514 * Blur event handler, bound to drag handle for keyboard support.
516 Drupal
.tableDrag
.prototype.dropRow = function (event
, self
) {
517 // Drop row functionality shared between mouseup and blur events.
518 if (self
.rowObject
!= null) {
519 var droppedRow
= self
.rowObject
.element
;
520 // The row is already in the right place so we just release it.
521 if (self
.rowObject
.changed
== true) {
522 // Update the fields in the dropped row.
523 self
.updateFields(droppedRow
);
525 // If a setting exists for affecting the entire group, update all the
526 // fields in the entire dragged group.
527 for (var group
in self
.tableSettings
) {
528 var rowSettings
= self
.rowSettings(group
, droppedRow
);
529 if (rowSettings
.relationship
== 'group') {
530 for (var n
in self
.rowObject
.children
) {
531 self
.updateField(self
.rowObject
.children
[n
], group
);
536 self
.rowObject
.markChanged();
537 if (self
.changed
== false) {
538 $(Drupal
.theme('tableDragChangedWarning')).insertBefore(self
.table
).hide().fadeIn('slow');
543 if (self
.indentEnabled
) {
544 self
.rowObject
.removeIndentClasses();
546 if (self
.oldRowElement
) {
547 $(self
.oldRowElement
).removeClass('drag-previous');
549 $(droppedRow
).removeClass('drag').addClass('drag-previous');
550 self
.oldRowElement
= droppedRow
;
552 self
.rowObject
= null;
555 // Functionality specific only to mouseup event.
556 if (self
.dragObject
!= null) {
557 $('.tabledrag-handle', droppedRow
).removeClass('tabledrag-handle-hover');
559 self
.dragObject
= null;
560 $('body').removeClass('drag');
561 clearInterval(self
.scrollInterval
);
563 // Hack for IE6 that flickers uncontrollably if select lists are moved.
564 if (navigator
.userAgent
.indexOf('MSIE 6.') != -1) {
565 $('select', this.table
).css('display', 'block');
571 * Get the mouse coordinates from the event (allowing for browser differences).
573 Drupal
.tableDrag
.prototype.mouseCoords = function (event
) {
574 if (event
.pageX
|| event
.pageY
) {
575 return { x
: event
.pageX
, y
: event
.pageY
};
578 x
: event
.clientX
+ document
.body
.scrollLeft
- document
.body
.clientLeft
,
579 y
: event
.clientY
+ document
.body
.scrollTop
- document
.body
.clientTop
584 * Given a target element and a mouse event, get the mouse offset from that
585 * element. To do this we need the element's position and the mouse position.
587 Drupal
.tableDrag
.prototype.getMouseOffset = function (target
, event
) {
588 var docPos
= $(target
).offset();
589 var mousePos
= this.mouseCoords(event
);
590 return { x
: mousePos
.x
- docPos
.left
, y
: mousePos
.y
- docPos
.top
};
594 * Find the row the mouse is currently over. This row is then taken and swapped
595 * with the one being dragged.
598 * The x coordinate of the mouse on the page (not the screen).
600 * The y coordinate of the mouse on the page (not the screen).
602 Drupal
.tableDrag
.prototype.findDropTargetRow = function (x
, y
) {
603 var rows
= $(this.table
.tBodies
[0].rows
).not(':hidden');
604 for (var n
= 0; n
< rows
.length
; n
++) {
607 var rowY
= $(row
).offset().top
;
608 // Because Safari does not report offsetHeight on table rows, but does on
609 // table cells, grab the firstChild of the row and use that instead.
610 // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari.
611 if (row
.offsetHeight
== 0) {
612 var rowHeight
= parseInt(row
.firstChild
.offsetHeight
, 10) / 2;
616 var rowHeight
= parseInt(row
.offsetHeight
, 10) / 2;
619 // Because we always insert before, we need to offset the height a bit.
620 if ((y
> (rowY
- rowHeight
)) && (y
< (rowY
+ rowHeight
))) {
621 if (this.indentEnabled
) {
622 // Check that this row is not a child of the row being dragged.
623 for (var n
in this.rowObject
.group
) {
624 if (this.rowObject
.group
[n
] == row
) {
630 // Do not allow a row to be swapped with itself.
631 if (row
== this.rowObject
.element
) {
636 // Check that swapping with this row is allowed.
637 if (!this.rowObject
.isValidSwap(row
)) {
641 // We may have found the row the mouse just passed over, but it doesn't
642 // take into account hidden rows. Skip backwards until we find a draggable
644 while ($(row
).is(':hidden') && $(row
).prev('tr').is(':hidden')) {
645 row
= $(row
).prev('tr').get(0);
654 * After the row is dropped, update the table fields according to the settings
655 * set for this table.
658 * DOM object for the row that was just dropped.
660 Drupal
.tableDrag
.prototype.updateFields = function (changedRow
) {
661 for (var group
in this.tableSettings
) {
662 // Each group may have a different setting for relationship, so we find
663 // the source rows for each separately.
664 this.updateField(changedRow
, group
);
669 * After the row is dropped, update a single table field according to specific
673 * DOM object for the row that was just dropped.
675 * The settings group on which field updates will occur.
677 Drupal
.tableDrag
.prototype.updateField = function (changedRow
, group
) {
678 var rowSettings
= this.rowSettings(group
, changedRow
);
680 // Set the row as its own target.
681 if (rowSettings
.relationship
== 'self' || rowSettings
.relationship
== 'group') {
682 var sourceRow
= changedRow
;
684 // Siblings are easy, check previous and next rows.
685 else if (rowSettings
.relationship
== 'sibling') {
686 var previousRow
= $(changedRow
).prev('tr').get(0);
687 var nextRow
= $(changedRow
).next('tr').get(0);
688 var sourceRow
= changedRow
;
689 if ($(previousRow
).is('.draggable') && $('.' + group
, previousRow
).length
) {
690 if (this.indentEnabled
) {
691 if ($('.indentations', previousRow
).size() == $('.indentations', changedRow
)) {
692 sourceRow
= previousRow
;
696 sourceRow
= previousRow
;
699 else if ($(nextRow
).is('.draggable') && $('.' + group
, nextRow
).length
) {
700 if (this.indentEnabled
) {
701 if ($('.indentations', nextRow
).size() == $('.indentations', changedRow
)) {
710 // Parents, look up the tree until we find a field not in this group.
711 // Go up as many parents as indentations in the changed row.
712 else if (rowSettings
.relationship
== 'parent') {
713 var previousRow
= $(changedRow
).prev('tr');
714 while (previousRow
.length
&& $('.indentation', previousRow
).length
>= this.rowObject
.indents
) {
715 previousRow
= previousRow
.prev('tr');
717 // If we found a row.
718 if (previousRow
.length
) {
719 sourceRow
= previousRow
[0];
721 // Otherwise we went all the way to the left of the table without finding
722 // a parent, meaning this item has been placed at the root level.
724 // Use the first row in the table as source, because it's guaranteed to
725 // be at the root level. Find the first item, then compare this row
726 // against it as a sibling.
727 sourceRow
= $(this.table
).find('tr.draggable:first').get(0);
728 if (sourceRow
== this.rowObject
.element
) {
729 sourceRow
= $(this.rowObject
.group
[this.rowObject
.group
.length
- 1]).next('tr.draggable').get(0);
731 var useSibling
= true;
735 // Because we may have moved the row from one category to another,
736 // take a look at our sibling and borrow its sources and targets.
737 this.copyDragClasses(sourceRow
, changedRow
, group
);
738 rowSettings
= this.rowSettings(group
, changedRow
);
740 // In the case that we're looking for a parent, but the row is at the top
741 // of the tree, copy our sibling's values.
743 rowSettings
.relationship
= 'sibling';
744 rowSettings
.source
= rowSettings
.target
;
747 var targetClass
= '.' + rowSettings
.target
;
748 var targetElement
= $(targetClass
, changedRow
).get(0);
750 // Check if a target element exists in this row.
752 var sourceClass
= '.' + rowSettings
.source
;
753 var sourceElement
= $(sourceClass
, sourceRow
).get(0);
754 switch (rowSettings
.action
) {
756 // Get the depth of the target row.
757 targetElement
.value
= $('.indentation', $(sourceElement
).parents('tr:first')).size();
761 targetElement
.value
= sourceElement
.value
;
764 var siblings
= this.rowObject
.findSiblings(rowSettings
);
765 if ($(targetElement
).is('select')) {
766 // Get a list of acceptable values.
768 $('option', targetElement
).each(function () {
769 values
.push(this.value
);
771 var maxVal
= values
[values
.length
- 1];
772 // Populate the values in the siblings.
773 $(targetClass
, siblings
).each(function () {
774 // If there are more items than possible values, assign the maximum value to the row.
775 if (values
.length
> 0) {
776 this.value
= values
.shift();
784 // Assume a numeric input field.
785 var weight
= parseInt($(targetClass
, siblings
[0]).val(), 10) || 0;
786 $(targetClass
, siblings
).each(function () {
797 * Copy all special tableDrag classes from one row's form elements to a
798 * different one, removing any special classes that the destination row
801 Drupal
.tableDrag
.prototype.copyDragClasses = function (sourceRow
, targetRow
, group
) {
802 var sourceElement
= $('.' + group
, sourceRow
);
803 var targetElement
= $('.' + group
, targetRow
);
804 if (sourceElement
.length
&& targetElement
.length
) {
805 targetElement
[0].className
= sourceElement
[0].className
;
809 Drupal
.tableDrag
.prototype.checkScroll = function (cursorY
) {
810 var de
= document
.documentElement
;
811 var b
= document
.body
;
813 var windowHeight
= this.windowHeight
= window
.innerHeight
|| (de
.clientHeight
&& de
.clientWidth
!= 0 ? de
.clientHeight
: b
.offsetHeight
);
814 var scrollY
= this.scrollY
= (document
.all
? (!de
.scrollTop
? b
.scrollTop
: de
.scrollTop
) : (window
.pageYOffset
? window
.pageYOffset
: window
.scrollY
));
815 var trigger
= this.scrollSettings
.trigger
;
818 // Return a scroll speed relative to the edge of the screen.
819 if (cursorY
- scrollY
> windowHeight
- trigger
) {
820 delta
= trigger
/ (windowHeight
+ scrollY
- cursorY
);
821 delta
= (delta
> 0 && delta
< trigger
) ? delta
: trigger
;
822 return delta
* this.scrollSettings
.amount
;
824 else if (cursorY
- scrollY
< trigger
) {
825 delta
= trigger
/ (cursorY
- scrollY
);
826 delta
= (delta
> 0 && delta
< trigger
) ? delta
: trigger
;
827 return -delta
* this.scrollSettings
.amount
;
831 Drupal
.tableDrag
.prototype.setScroll = function (scrollAmount
) {
834 this.scrollInterval
= setInterval(function () {
835 // Update the scroll values stored in the object.
836 self
.checkScroll(self
.currentMouseCoords
.y
);
837 var aboveTable
= self
.scrollY
> self
.table
.topY
;
838 var belowTable
= self
.scrollY
+ self
.windowHeight
< self
.table
.bottomY
;
839 if (scrollAmount
> 0 && belowTable
|| scrollAmount
< 0 && aboveTable
) {
840 window
.scrollBy(0, scrollAmount
);
842 }, this.scrollSettings
.interval
);
845 Drupal
.tableDrag
.prototype.restripeTable = function () {
846 // :even and :odd are reversed because jQuery counts from 0 and
847 // we count from 1, so we're out of sync.
848 // Match immediate children of the parent element to allow nesting.
849 $('> tbody > tr.draggable:visible, > tr.draggable:visible', this.table
)
850 .removeClass('odd even')
851 .filter(':odd').addClass('even').end()
852 .filter(':even').addClass('odd');
856 * Stub function. Allows a custom handler when a row begins dragging.
858 Drupal
.tableDrag
.prototype.onDrag = function () {
863 * Stub function. Allows a custom handler when a row is dropped.
865 Drupal
.tableDrag
.prototype.onDrop = function () {
870 * Constructor to make a new object to manipulate a table row.
873 * The DOM element for the table row we will be manipulating.
875 * The method in which this row is being moved. Either 'keyboard' or 'mouse'.
876 * @param indentEnabled
877 * Whether the containing table uses indentations. Used for optimizations.
879 * The maximum amount of indentations this row may contain.
881 * Whether we want to add classes to this row to indicate child relationships.
883 Drupal
.tableDrag
.prototype.row = function (tableRow
, method
, indentEnabled
, maxDepth
, addClasses
) {
884 this.element
= tableRow
;
885 this.method
= method
;
886 this.group
= [tableRow
];
887 this.groupDepth
= $('.indentation', tableRow
).size();
888 this.changed
= false;
889 this.table
= $(tableRow
).parents('table:first').get(0);
890 this.indentEnabled
= indentEnabled
;
891 this.maxDepth
= maxDepth
;
892 this.direction
= ''; // Direction the row is being moved.
894 if (this.indentEnabled
) {
895 this.indents
= $('.indentation', tableRow
).size();
896 this.children
= this.findChildren(addClasses
);
897 this.group
= $.merge(this.group
, this.children
);
898 // Find the depth of this entire group.
899 for (var n
= 0; n
< this.group
.length
; n
++) {
900 this.groupDepth
= Math
.max($('.indentation', this.group
[n
]).size(), this.groupDepth
);
906 * Find all children of rowObject by indentation.
909 * Whether we want to add classes to this row to indicate child relationships.
911 Drupal
.tableDrag
.prototype.row
.prototype.findChildren = function (addClasses
) {
912 var parentIndentation
= this.indents
;
913 var currentRow
= $(this.element
, this.table
).next('tr.draggable');
916 while (currentRow
.length
) {
917 var rowIndentation
= $('.indentation', currentRow
).length
;
918 // A greater indentation indicates this is a child.
919 if (rowIndentation
> parentIndentation
) {
921 rows
.push(currentRow
[0]);
923 $('.indentation', currentRow
).each(function (indentNum
) {
924 if (child
== 1 && (indentNum
== parentIndentation
)) {
925 $(this).addClass('tree-child-first');
927 if (indentNum
== parentIndentation
) {
928 $(this).addClass('tree-child');
930 else if (indentNum
> parentIndentation
) {
931 $(this).addClass('tree-child-horizontal');
939 currentRow
= currentRow
.next('tr.draggable');
941 if (addClasses
&& rows
.length
) {
942 $('.indentation:nth-child(' + (parentIndentation
+ 1) + ')', rows
[rows
.length
- 1]).addClass('tree-child-last');
948 * Ensure that two rows are allowed to be swapped.
951 * DOM object for the row being considered for swapping.
953 Drupal
.tableDrag
.prototype.row
.prototype.isValidSwap = function (row
) {
954 if (this.indentEnabled
) {
955 var prevRow
, nextRow
;
956 if (this.direction
== 'down') {
958 nextRow
= $(row
).next('tr').get(0);
961 prevRow
= $(row
).prev('tr').get(0);
964 this.interval
= this.validIndentInterval(prevRow
, nextRow
);
966 // We have an invalid swap if the valid indentations interval is empty.
967 if (this.interval
.min
> this.interval
.max
) {
972 // Do not let an un-draggable first row have anything put before it.
973 if (this.table
.tBodies
[0].rows
[0] == row
&& $(row
).is(':not(.draggable)')) {
981 * Perform the swap between two rows.
984 * Whether the swap will occur 'before' or 'after' the given row.
986 * DOM element what will be swapped with the row group.
988 Drupal
.tableDrag
.prototype.row
.prototype.swap = function (position
, row
) {
989 Drupal
.detachBehaviors(this.group
, Drupal
.settings
, 'move');
990 $(row
)[position
](this.group
);
991 Drupal
.attachBehaviors(this.group
, Drupal
.settings
);
997 * Determine the valid indentations interval for the row at a given position
1001 * DOM object for the row before the tested position
1002 * (or null for first position in the table).
1004 * DOM object for the row after the tested position
1005 * (or null for last position in the table).
1007 Drupal
.tableDrag
.prototype.row
.prototype.validIndentInterval = function (prevRow
, nextRow
) {
1008 var minIndent
, maxIndent
;
1010 // Minimum indentation:
1011 // Do not orphan the next row.
1012 minIndent
= nextRow
? $('.indentation', nextRow
).size() : 0;
1014 // Maximum indentation:
1015 if (!prevRow
|| $(prevRow
).is(':not(.draggable)') || $(this.element
).is('.tabledrag-root')) {
1017 // - the first row in the table,
1018 // - rows dragged below a non-draggable row,
1023 // Do not go deeper than as a child of the previous row.
1024 maxIndent
= $('.indentation', prevRow
).size() + ($(prevRow
).is('.tabledrag-leaf') ? 0 : 1);
1025 // Limit by the maximum allowed depth for the table.
1026 if (this.maxDepth
) {
1027 maxIndent
= Math
.min(maxIndent
, this.maxDepth
- (this.groupDepth
- this.indents
));
1031 return { 'min': minIndent
, 'max': maxIndent
};
1035 * Indent a row within the legal bounds of the table.
1038 * The number of additional indentations proposed for the row (can be
1039 * positive or negative). This number will be adjusted to nearest valid
1040 * indentation level for the row.
1042 Drupal
.tableDrag
.prototype.row
.prototype.indent = function (indentDiff
) {
1043 // Determine the valid indentations interval if not available yet.
1044 if (!this.interval
) {
1045 prevRow
= $(this.element
).prev('tr').get(0);
1046 nextRow
= $(this.group
).filter(':last').next('tr').get(0);
1047 this.interval
= this.validIndentInterval(prevRow
, nextRow
);
1050 // Adjust to the nearest valid indentation.
1051 var indent
= this.indents
+ indentDiff
;
1052 indent
= Math
.max(indent
, this.interval
.min
);
1053 indent
= Math
.min(indent
, this.interval
.max
);
1054 indentDiff
= indent
- this.indents
;
1056 for (var n
= 1; n
<= Math
.abs(indentDiff
); n
++) {
1057 // Add or remove indentations.
1058 if (indentDiff
< 0) {
1059 $('.indentation:first', this.group
).remove();
1063 $('td:first', this.group
).prepend(Drupal
.theme('tableDragIndentation'));
1068 // Update indentation for this row.
1069 this.changed
= true;
1070 this.groupDepth
+= indentDiff
;
1078 * Find all siblings for a row, either according to its subgroup or indentation.
1079 * Note that the passed-in row is included in the list of siblings.
1082 * The field settings we're using to identify what constitutes a sibling.
1084 Drupal
.tableDrag
.prototype.row
.prototype.findSiblings = function (rowSettings
) {
1086 var directions
= ['prev', 'next'];
1087 var rowIndentation
= this.indents
;
1088 for (var d
= 0; d
< directions
.length
; d
++) {
1089 var checkRow
= $(this.element
)[directions
[d
]]();
1090 while (checkRow
.length
) {
1091 // Check that the sibling contains a similar target field.
1092 if ($('.' + rowSettings
.target
, checkRow
)) {
1093 // Either add immediately if this is a flat table, or check to ensure
1094 // that this row has the same level of indentation.
1095 if (this.indentEnabled
) {
1096 var checkRowIndentation
= $('.indentation', checkRow
).length
;
1099 if (!(this.indentEnabled
) || (checkRowIndentation
== rowIndentation
)) {
1100 siblings
.push(checkRow
[0]);
1102 else if (checkRowIndentation
< rowIndentation
) {
1103 // No need to keep looking for siblings when we get to a parent.
1110 checkRow
= $(checkRow
)[directions
[d
]]();
1112 // Since siblings are added in reverse order for previous, reverse the
1113 // completed list of previous siblings. Add the current row and continue.
1114 if (directions
[d
] == 'prev') {
1116 siblings
.push(this.element
);
1123 * Remove indentation helper classes from the current row group.
1125 Drupal
.tableDrag
.prototype.row
.prototype.removeIndentClasses = function () {
1126 for (var n
in this.children
) {
1127 $('.indentation', this.children
[n
])
1128 .removeClass('tree-child')
1129 .removeClass('tree-child-first')
1130 .removeClass('tree-child-last')
1131 .removeClass('tree-child-horizontal');
1136 * Add an asterisk or other marker to the changed row.
1138 Drupal
.tableDrag
.prototype.row
.prototype.markChanged = function () {
1139 var marker
= Drupal
.theme('tableDragChangedMarker');
1140 var cell
= $('td:first', this.element
);
1141 if ($('span.tabledrag-changed', cell
).length
== 0) {
1142 cell
.append(marker
);
1147 * Stub function. Allows a custom handler when a row is indented.
1149 Drupal
.tableDrag
.prototype.row
.prototype.onIndent = function () {
1154 * Stub function. Allows a custom handler when a row is swapped.
1156 Drupal
.tableDrag
.prototype.row
.prototype.onSwap = function (swappedRow
) {
1160 Drupal
.theme
.prototype.tableDragChangedMarker = function () {
1161 return '<span class="warning tabledrag-changed">*</span>';
1164 Drupal
.theme
.prototype.tableDragIndentation = function () {
1165 return '<div class="indentation"> </div>';
1168 Drupal
.theme
.prototype.tableDragChangedWarning = function () {
1169 return '<div class="tabledrag-changed-warning messages warning">' + Drupal
.theme('tableDragChangedMarker') + ' ' + Drupal
.t('Changes made in this table will not be saved until the form is submitted.') + '</div>';