/[drupal]/drupal/misc/tabledrag.js
ViewVC logotype

Contents of /drupal/misc/tabledrag.js

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.32 - (show annotations) (download) (as text)
Tue Nov 3 05:34:37 2009 UTC (3 weeks, 3 days ago) by webchick
Branch: MAIN
CVS Tags: DRUPAL-7-0-UNSTABLE-10, HEAD
Changes since 1.31: +3 -1 lines
File MIME type: text/javascript
#561726 by effulgentsia, TwoD, and sun: Make ajax.js and tabledrag.js implement Drupal.detachBehaviors().
1 // $Id: tabledrag.js,v 1.31 2009/09/20 19:14:40 dries Exp $
2 (function ($) {
3
4 /**
5 * Drag and drop table rows with field manipulation.
6 *
7 * Using the drupal_add_tabledrag() function, any table with weights or parent
8 * relationships may be made into draggable tables. Columns containing a field
9 * may optionally be hidden, providing a better user experience.
10 *
11 * Created tableDrag instances may be modified with custom behaviors by
12 * overriding the .onDrag, .onDrop, .row.onSwap, and .row.onIndent methods.
13 * See blocks.js for an example of adding additional functionality to tableDrag.
14 */
15 Drupal.behaviors.tableDrag = {
16 attach: function (context, settings) {
17 for (var base in settings.tableDrag) {
18 $('#' + base, context).once('tabledrag', function () {
19 // Create the new tableDrag instance. Save in the Drupal variable
20 // to allow other scripts access to the object.
21 Drupal.tableDrag[base] = new Drupal.tableDrag(this, settings.tableDrag[base]);
22 });
23 }
24 }
25 };
26
27 /**
28 * Constructor for the tableDrag object. Provides table and field manipulation.
29 *
30 * @param table
31 * DOM object for the table to be made draggable.
32 * @param tableSettings
33 * Settings for the table added via drupal_add_dragtable().
34 */
35 Drupal.tableDrag = function (table, tableSettings) {
36 var self = this;
37
38 // Required object variables.
39 this.table = table;
40 this.tableSettings = tableSettings;
41 this.dragObject = null; // Used to hold information about a current drag operation.
42 this.rowObject = null; // Provides operations for row manipulation.
43 this.oldRowElement = null; // Remember the previous element.
44 this.oldY = 0; // Used to determine up or down direction from last mouse move.
45 this.changed = false; // Whether anything in the entire table has changed.
46 this.maxDepth = 0; // Maximum amount of allowed parenting.
47 this.rtl = $(this.table).css('direction') == 'rtl' ? -1 : 1; // Direction of the table.
48
49 // Configure the scroll settings.
50 this.scrollSettings = { amount: 4, interval: 50, trigger: 70 };
51 this.scrollInterval = null;
52 this.scrollY = 0;
53 this.windowHeight = 0;
54
55 // Check this table's settings to see if there are parent relationships in
56 // this table. For efficiency, large sections of code can be skipped if we
57 // don't need to track horizontal movement and indentations.
58 this.indentEnabled = false;
59 for (group in tableSettings) {
60 for (n in tableSettings[group]) {
61 if (tableSettings[group][n].relationship == 'parent') {
62 this.indentEnabled = true;
63 }
64 if (tableSettings[group][n].limit > 0) {
65 this.maxDepth = tableSettings[group][n].limit;
66 }
67 }
68 }
69 if (this.indentEnabled) {
70 this.indentCount = 1; // Total width of indents, set in makeDraggable.
71 // Find the width of indentations to measure mouse movements against.
72 // Because the table doesn't need to start with any indentations, we
73 // manually append 2 indentations in the first draggable row, measure
74 // the offset, then remove.
75 var indent = Drupal.theme('tableDragIndentation');
76 // Match immediate children of the parent element to allow nesting.
77 var testCell = $('> tbody > tr.draggable:first td:first, > tr.draggable:first td:first', table).prepend(indent).prepend(indent);
78 this.indentAmount = $('.indentation', testCell).get(1).offsetLeft - $('.indentation', testCell).get(0).offsetLeft;
79 $('.indentation', testCell).slice(0, 2).remove();
80 }
81
82 // Make each applicable row draggable.
83 // Match immediate children of the parent element to allow nesting.
84 $('> tr.draggable, > tbody > tr.draggable', table).each(function() { self.makeDraggable(this); });
85
86 // Hide columns containing affected form elements.
87 this.hideColumns();
88
89 // Add mouse bindings to the document. The self variable is passed along
90 // as event handlers do not have direct access to the tableDrag object.
91 $(document).bind('mousemove', function (event) { return self.dragRow(event, self); });
92 $(document).bind('mouseup', function (event) { return self.dropRow(event, self); });
93 };
94
95 /**
96 * Hide the columns containing form elements according to the settings for
97 * this tableDrag instance.
98 */
99 Drupal.tableDrag.prototype.hideColumns = function () {
100 for (var group in this.tableSettings) {
101 // Find the first field in this group.
102 for (var d in this.tableSettings[group]) {
103 var field = $('.' + this.tableSettings[group][d].target + ':first', this.table);
104 if (field.size() && this.tableSettings[group][d].hidden) {
105 var hidden = this.tableSettings[group][d].hidden;
106 var cell = field.parents('td:first');
107 break;
108 }
109 }
110
111 // Hide the column containing this field.
112 if (hidden && cell[0] && cell.css('display') != 'none') {
113 // Add 1 to our indexes. The nth-child selector is 1 based, not 0 based.
114 // Match immediate children of the parent element to allow nesting.
115 var columnIndex = $('> td', cell.parent()).index(cell.get(0)) + 1;
116 var headerIndex = $('> td:not(:hidden)', cell.parent()).index(cell.get(0)) + 1;
117 $('> thead > tr, > tbody > tr, > tr', this.table).each(function(){
118 var row = $(this);
119 var parentTag = row.parent().get(0).tagName.toLowerCase();
120 var index = (parentTag == 'thead') ? headerIndex : columnIndex;
121
122 // Adjust the index to take into account colspans.
123 row.children().each(function (n) {
124 if (n < index) {
125 index -= (this.colSpan && this.colSpan > 1) ? this.colSpan - 1 : 0;
126 }
127 });
128 if (index > 0) {
129 cell = row.children(':nth-child(' + index + ')');
130 if (cell[0].colSpan > 1) {
131 // If this cell has a colspan, simply reduce it.
132 cell[0].colSpan = cell[0].colSpan - 1;
133 }
134 else {
135 // Hide table body cells, but remove table header cells entirely
136 // (Safari doesn't hide properly).
137 parentTag == 'thead' ? cell.remove() : cell.css('display', 'none');
138 }
139 }
140 });
141 }
142 }
143 };
144
145 /**
146 * Find the target used within a particular row and group.
147 */
148 Drupal.tableDrag.prototype.rowSettings = function (group, row) {
149 var field = $('.' + group, row);
150 for (delta in this.tableSettings[group]) {
151 var targetClass = this.tableSettings[group][delta].target;
152 if (field.is('.' + targetClass)) {
153 // Return a copy of the row settings.
154 var rowSettings = {};
155 for (var n in this.tableSettings[group][delta]) {
156 rowSettings[n] = this.tableSettings[group][delta][n];
157 }
158 return rowSettings;
159 }
160 }
161 };
162
163 /**
164 * Take an item and add event handlers to make it become draggable.
165 */
166 Drupal.tableDrag.prototype.makeDraggable = function (item) {
167 var self = this;
168
169 // Create the handle.
170 var handle = $('<a href="#" class="tabledrag-handle"><div class="handle">&nbsp;</div></a>').attr('title', Drupal.t('Drag to re-order'));
171 // Insert the handle after indentations (if any).
172 if ($('td:first .indentation:last', item).after(handle).size()) {
173 // Update the total width of indentation in this entire table.
174 self.indentCount = Math.max($('.indentation', item).size(), self.indentCount);
175 }
176 else {
177 $('td:first', item).prepend(handle);
178 }
179
180 // Add hover action for the handle.
181 handle.hover(function () {
182 self.dragObject == null ? $(this).addClass('tabledrag-handle-hover') : null;
183 }, function () {
184 self.dragObject == null ? $(this).removeClass('tabledrag-handle-hover') : null;
185 });
186
187 // Add the mousedown action for the handle.
188 handle.mousedown(function (event) {
189 // Create a new dragObject recording the event information.
190 self.dragObject = {};
191 self.dragObject.initMouseOffset = self.getMouseOffset(item, event);
192 self.dragObject.initMouseCoords = self.mouseCoords(event);
193 if (self.indentEnabled) {
194 self.dragObject.indentMousePos = self.dragObject.initMouseCoords;
195 }
196
197 // If there's a lingering row object from the keyboard, remove its focus.
198 if (self.rowObject) {
199 $('a.tabledrag-handle', self.rowObject.element).blur();
200 }
201
202 // Create a new rowObject for manipulation of this row.
203 self.rowObject = new self.row(item, 'mouse', self.indentEnabled, self.maxDepth, true);
204
205 // Save the position of the table.
206 self.table.topY = $(self.table).offset().top;
207 self.table.bottomY = self.table.topY + self.table.offsetHeight;
208
209 // Add classes to the handle and row.
210 $(this).addClass('tabledrag-handle-hover');
211 $(item).addClass('drag');
212
213 // Set the document to use the move cursor during drag.
214 $('body').addClass('drag');
215 if (self.oldRowElement) {
216 $(self.oldRowElement).removeClass('drag-previous');
217 }
218
219 // Hack for IE6 that flickers uncontrollably if select lists are moved.
220 if (navigator.userAgent.indexOf('MSIE 6.') != -1) {
221 $('select', this.table).css('display', 'none');
222 }
223
224 // Hack for Konqueror, prevent the blur handler from firing.
225 // Konqueror always gives links focus, even after returning false on mousedown.
226 self.safeBlur = false;
227
228 // Call optional placeholder function.
229 self.onDrag();
230 return false;
231 });
232
233 // Prevent the anchor tag from jumping us to the top of the page.
234 handle.click(function () {
235 return false;
236 });
237
238 // Similar to the hover event, add a class when the handle is focused.
239 handle.focus(function () {
240 $(this).addClass('tabledrag-handle-hover');
241 self.safeBlur = true;
242 });
243
244 // Remove the handle class on blur and fire the same function as a mouseup.
245 handle.blur(function (event) {
246 $(this).removeClass('tabledrag-handle-hover');
247 if (self.rowObject && self.safeBlur) {
248 self.dropRow(event, self);
249 }
250 });
251
252 // Add arrow-key support to the handle.
253 handle.keydown(function (event) {
254 // If a rowObject doesn't yet exist and this isn't the tab key.
255 if (event.keyCode != 9 && !self.rowObject) {
256 self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true);
257 }
258
259 var keyChange = false;
260 switch (event.keyCode) {
261 case 37: // Left arrow.
262 case 63234: // Safari left arrow.
263 keyChange = true;
264 self.rowObject.indent(-1 * self.rtl);
265 break;
266 case 38: // Up arrow.
267 case 63232: // Safari up arrow.
268 var previousRow = $(self.rowObject.element).prev('tr').get(0);
269 while (previousRow && $(previousRow).is(':hidden')) {
270 previousRow = $(previousRow).prev('tr').get(0);
271 }
272 if (previousRow) {
273 self.safeBlur = false; // Do not allow the onBlur cleanup.
274 self.rowObject.direction = 'up';
275 keyChange = true;
276
277 if ($(item).is('.tabledrag-root')) {
278 // Swap with the previous top-level row.
279 var groupHeight = 0;
280 while (previousRow && $('.indentation', previousRow).size()) {
281 previousRow = $(previousRow).prev('tr').get(0);
282 groupHeight += $(previousRow).is(':hidden') ? 0 : previousRow.offsetHeight;
283 }
284 if (previousRow) {
285 self.rowObject.swap('before', previousRow);
286 // No need to check for indentation, 0 is the only valid one.
287 window.scrollBy(0, -groupHeight);
288 }
289 }
290 else if (self.table.tBodies[0].rows[0] != previousRow || $(previousRow).is('.draggable')) {
291 // Swap with the previous row (unless previous row is the first one
292 // and undraggable).
293 self.rowObject.swap('before', previousRow);
294 self.rowObject.interval = null;
295 self.rowObject.indent(0);
296 window.scrollBy(0, -parseInt(item.offsetHeight, 10));
297 }
298 handle.get(0).focus(); // Regain focus after the DOM manipulation.
299 }
300 break;
301 case 39: // Right arrow.
302 case 63235: // Safari right arrow.
303 keyChange = true;
304 self.rowObject.indent(1 * self.rtl);
305 break;
306 case 40: // Down arrow.
307 case 63233: // Safari down arrow.
308 var nextRow = $(self.rowObject.group).filter(':last').next('tr').get(0);
309 while (nextRow && $(nextRow).is(':hidden')) {
310 nextRow = $(nextRow).next('tr').get(0);
311 }
312 if (nextRow) {
313 self.safeBlur = false; // Do not allow the onBlur cleanup.
314 self.rowObject.direction = 'down';
315 keyChange = true;
316
317 if ($(item).is('.tabledrag-root')) {
318 // Swap with the next group (necessarily a top-level one).
319 var groupHeight = 0;
320 nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false);
321 if (nextGroup) {
322 $(nextGroup.group).each(function () {
323 groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight;
324 });
325 nextGroupRow = $(nextGroup.group).filter(':last').get(0);
326 self.rowObject.swap('after', nextGroupRow);
327 // No need to check for indentation, 0 is the only valid one.
328 window.scrollBy(0, parseInt(groupHeight, 10));
329 }
330 }
331 else {
332 // Swap with the next row.
333 self.rowObject.swap('after', nextRow);
334 self.rowObject.interval = null;
335 self.rowObject.indent(0);
336 window.scrollBy(0, parseInt(item.offsetHeight, 10));
337 }
338 handle.get(0).focus(); // Regain focus after the DOM manipulation.
339 }
340 break;
341 }
342
343 if (self.rowObject && self.rowObject.changed == true) {
344 $(item).addClass('drag');
345 if (self.oldRowElement) {
346 $(self.oldRowElement).removeClass('drag-previous');
347 }
348 self.oldRowElement = item;
349 self.restripeTable();
350 self.onDrag();
351 }
352
353 // Returning false if we have an arrow key to prevent scrolling.
354 if (keyChange) {
355 return false;
356 }
357 });
358
359 // Compatibility addition, return false on keypress to prevent unwanted scrolling.
360 // IE and Safari will suppress scrolling on keydown, but all other browsers
361 // need to return false on keypress. http://www.quirksmode.org/js/keys.html
362 handle.keypress(function (event) {
363 switch (event.keyCode) {
364 case 37: // Left arrow.
365 case 38: // Up arrow.
366 case 39: // Right arrow.
367 case 40: // Down arrow.
368 return false;
369 }
370 });
371 };
372
373 /**
374 * Mousemove event handler, bound to document.
375 */
376 Drupal.tableDrag.prototype.dragRow = function (event, self) {
377 if (self.dragObject) {
378 self.currentMouseCoords = self.mouseCoords(event);
379
380 var y = self.currentMouseCoords.y - self.dragObject.initMouseOffset.y;
381 var x = self.currentMouseCoords.x - self.dragObject.initMouseOffset.x;
382
383 // Check for row swapping and vertical scrolling.
384 if (y != self.oldY) {
385 self.rowObject.direction = y > self.oldY ? 'down' : 'up';
386 self.oldY = y; // Update the old value.
387
388 // Check if the window should be scrolled (and how fast).
389 var scrollAmount = self.checkScroll(self.currentMouseCoords.y);
390 // Stop any current scrolling.
391 clearInterval(self.scrollInterval);
392 // Continue scrolling if the mouse has moved in the scroll direction.
393 if (scrollAmount > 0 && self.rowObject.direction == 'down' || scrollAmount < 0 && self.rowObject.direction == 'up') {
394 self.setScroll(scrollAmount);
395 }
396
397 // If we have a valid target, perform the swap and restripe the table.
398 var currentRow = self.findDropTargetRow(x, y);
399 if (currentRow) {
400 if (self.rowObject.direction == 'down') {
401 self.rowObject.swap('after', currentRow, self);
402 }
403 else {
404 self.rowObject.swap('before', currentRow, self);
405 }
406 self.restripeTable();
407 }
408 }
409
410 // Similar to row swapping, handle indentations.
411 if (self.indentEnabled) {
412 var xDiff = self.currentMouseCoords.x - self.dragObject.indentMousePos.x;
413 // Set the number of indentations the mouse has been moved left or right.
414 var indentDiff = Math.round(xDiff / self.indentAmount * self.rtl);
415 // Indent the row with our estimated diff, which may be further
416 // restricted according to the rows around this row.
417 var indentChange = self.rowObject.indent(indentDiff);
418 // Update table and mouse indentations.
419 self.dragObject.indentMousePos.x += self.indentAmount * indentChange * self.rtl;
420 self.indentCount = Math.max(self.indentCount, self.rowObject.indents);
421 }
422
423 return false;
424 }
425 };
426
427 /**
428 * Mouseup event handler, bound to document.
429 * Blur event handler, bound to drag handle for keyboard support.
430 */
431 Drupal.tableDrag.prototype.dropRow = function (event, self) {
432 // Drop row functionality shared between mouseup and blur events.
433 if (self.rowObject != null) {
434 var droppedRow = self.rowObject.element;
435 // The row is already in the right place so we just release it.
436 if (self.rowObject.changed == true) {
437 // Update the fields in the dropped row.
438 self.updateFields(droppedRow);
439
440 // If a setting exists for affecting the entire group, update all the
441 // fields in the entire dragged group.
442 for (var group in self.tableSettings) {
443 var rowSettings = self.rowSettings(group, droppedRow);
444 if (rowSettings.relationship == 'group') {
445 for (n in self.rowObject.children) {
446 self.updateField(self.rowObject.children[n], group);
447 }
448 }
449 }
450
451 self.rowObject.markChanged();
452 if (self.changed == false) {
453 $(Drupal.theme('tableDragChangedWarning')).insertBefore(self.table).hide().fadeIn('slow');
454 self.changed = true;
455 }
456 }
457
458 if (self.indentEnabled) {
459 self.rowObject.removeIndentClasses();
460 }
461 if (self.oldRowElement) {
462 $(self.oldRowElement).removeClass('drag-previous');
463 }
464 $(droppedRow).removeClass('drag').addClass('drag-previous');
465 self.oldRowElement = droppedRow;
466 self.onDrop();
467 self.rowObject = null;
468 }
469
470 // Functionality specific only to mouseup event.
471 if (self.dragObject != null) {
472 $('.tabledrag-handle', droppedRow).removeClass('tabledrag-handle-hover');
473
474 self.dragObject = null;
475 $('body').removeClass('drag');
476 clearInterval(self.scrollInterval);
477
478 // Hack for IE6 that flickers uncontrollably if select lists are moved.
479 if (navigator.userAgent.indexOf('MSIE 6.') != -1) {
480 $('select', this.table).css('display', 'block');
481 }
482 }
483 };
484
485 /**
486 * Get the mouse coordinates from the event (allowing for browser differences).
487 */
488 Drupal.tableDrag.prototype.mouseCoords = function (event) {
489 if (event.pageX || event.pageY) {
490 return { x: event.pageX, y: event.pageY };
491 }
492 return {
493 x: event.clientX + document.body.scrollLeft - document.body.clientLeft,
494 y: event.clientY + document.body.scrollTop - document.body.clientTop
495 };
496 };
497
498 /**
499 * Given a target element and a mouse event, get the mouse offset from that
500 * element. To do this we need the element's position and the mouse position.
501 */
502 Drupal.tableDrag.prototype.getMouseOffset = function (target, event) {
503 var docPos = $(target).offset();
504 var mousePos = this.mouseCoords(event);
505 return { x: mousePos.x - docPos.left, y: mousePos.y - docPos.top };
506 };
507
508 /**
509 * Find the row the mouse is currently over. This row is then taken and swapped
510 * with the one being dragged.
511 *
512 * @param x
513 * The x coordinate of the mouse on the page (not the screen).
514 * @param y
515 * The y coordinate of the mouse on the page (not the screen).
516 */
517 Drupal.tableDrag.prototype.findDropTargetRow = function (x, y) {
518 var rows = this.table.tBodies[0].rows;
519 for (var n = 0; n < rows.length; n++) {
520 var row = rows[n];
521 var indentDiff = 0;
522 var rowY = $(row).offset().top;
523 // Because Safari does not report offsetHeight on table rows, but does on
524 // table cells, grab the firstChild of the row and use that instead.
525 // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari.
526 if (row.offsetHeight == 0) {
527 var rowHeight = parseInt(row.firstChild.offsetHeight, 10) / 2;
528 }
529 // Other browsers.
530 else {
531 var rowHeight = parseInt(row.offsetHeight, 10) / 2;
532 }
533
534 // Because we always insert before, we need to offset the height a bit.
535 if ((y > (rowY - rowHeight)) && (y < (rowY + rowHeight))) {
536 if (this.indentEnabled) {
537 // Check that this row is not a child of the row being dragged.
538 for (n in this.rowObject.group) {
539 if (this.rowObject.group[n] == row) {
540 return null;
541 }
542 }
543 }
544 else {
545 // Do not allow a row to be swapped with itself.
546 if (row == this.rowObject.element) {
547 return null;
548 }
549 }
550
551 // Check that swapping with this row is allowed.
552 if (!this.rowObject.isValidSwap(row)) {
553 return null;
554 }
555
556 // We may have found the row the mouse just passed over, but it doesn't
557 // take into account hidden rows. Skip backwards until we find a draggable
558 // row.
559 while ($(row).is(':hidden') && $(row).prev('tr').is(':hidden')) {
560 row = $(row).prev('tr').get(0);
561 }
562 return row;
563 }
564 }
565 return null;
566 };
567
568 /**
569 * After the row is dropped, update the table fields according to the settings
570 * set for this table.
571 *
572 * @param changedRow
573 * DOM object for the row that was just dropped.
574 */
575 Drupal.tableDrag.prototype.updateFields = function (changedRow) {
576 for (var group in this.tableSettings) {
577 // Each group may have a different setting for relationship, so we find
578 // the source rows for each separately.
579 this.updateField(changedRow, group);
580 }
581 };
582
583 /**
584 * After the row is dropped, update a single table field according to specific
585 * settings.
586 *
587 * @param changedRow
588 * DOM object for the row that was just dropped.
589 * @param group
590 * The settings group on which field updates will occur.
591 */
592 Drupal.tableDrag.prototype.updateField = function (changedRow, group) {
593 var rowSettings = this.rowSettings(group, changedRow);
594
595 // Set the row as it's own target.
596 if (rowSettings.relationship == 'self' || rowSettings.relationship == 'group') {
597 var sourceRow = changedRow;
598 }
599 // Siblings are easy, check previous and next rows.
600 else if (rowSettings.relationship == 'sibling') {
601 var previousRow = $(changedRow).prev('tr').get(0);
602 var nextRow = $(changedRow).next('tr').get(0);
603 var sourceRow = changedRow;
604 if ($(previousRow).is('.draggable') && $('.' + group, previousRow).length) {
605 if (this.indentEnabled) {
606 if ($('.indentations', previousRow).size() == $('.indentations', changedRow)) {
607 sourceRow = previousRow;
608 }
609 }
610 else {
611 sourceRow = previousRow;
612 }
613 }
614 else if ($(nextRow).is('.draggable') && $('.' + group, nextRow).length) {
615 if (this.indentEnabled) {
616 if ($('.indentations', nextRow).size() == $('.indentations', changedRow)) {
617 sourceRow = nextRow;
618 }
619 }
620 else {
621 sourceRow = nextRow;
622 }
623 }
624 }
625 // Parents, look up the tree until we find a field not in this group.
626 // Go up as many parents as indentations in the changed row.
627 else if (rowSettings.relationship == 'parent') {
628 var previousRow = $(changedRow).prev('tr');
629 while (previousRow.length && $('.indentation', previousRow).length >= this.rowObject.indents) {
630 previousRow = previousRow.prev('tr');
631 }
632 // If we found a row.
633 if (previousRow.length) {
634 sourceRow = previousRow[0];
635 }
636 // Otherwise we went all the way to the left of the table without finding
637 // a parent, meaning this item has been placed at the root level.
638 else {
639 // Use the first row in the table as source, because it's guaranteed to
640 // be at the root level. Find the first item, then compare this row
641 // against it as a sibling.
642 sourceRow = $('tr.draggable:first').get(0);
643 if (sourceRow == this.rowObject.element) {
644 sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0);
645 }
646 var useSibling = true;
647 }
648 }
649
650 // Because we may have moved the row from one category to another,
651 // take a look at our sibling and borrow its sources and targets.
652 this.copyDragClasses(sourceRow, changedRow, group);
653 rowSettings = this.rowSettings(group, changedRow);
654
655 // In the case that we're looking for a parent, but the row is at the top
656 // of the tree, copy our sibling's values.
657 if (useSibling) {
658 rowSettings.relationship = 'sibling';
659 rowSettings.source = rowSettings.target;
660 }
661
662 var targetClass = '.' + rowSettings.target;
663 var targetElement = $(targetClass, changedRow).get(0);
664
665 // Check if a target element exists in this row.
666 if (targetElement) {
667 var sourceClass = '.' + rowSettings.source;
668 var sourceElement = $(sourceClass, sourceRow).get(0);
669 switch (rowSettings.action) {
670 case 'depth':
671 // Get the depth of the target row.
672 targetElement.value = $('.indentation', $(sourceElement).parents('tr:first')).size();
673 break;
674 case 'match':
675 // Update the value.
676 targetElement.value = sourceElement.value;
677 break;
678 case 'order':
679 var siblings = this.rowObject.findSiblings(rowSettings);
680 if ($(targetElement).is('select')) {
681 // Get a list of acceptable values.
682 var values = [];
683 $('option', targetElement).each(function () {
684 values.push(this.value);
685 });
686 var maxVal = values[values.length - 1];
687 // Populate the values in the siblings.
688 $(targetClass, siblings).each(function () {
689 // If there are more items than possible values, assign the maximum value to the row.
690 if (values.length > 0) {
691 this.value = values.shift();
692 }
693 else {
694 this.value = maxVal;
695 }
696 });
697 }
698 else {
699 // Assume a numeric input field.
700 var weight = parseInt($(targetClass, siblings[0]).val(), 10) || 0;
701 $(targetClass, siblings).each(function () {
702 this.value = weight;
703 weight++;
704 });
705 }
706 break;
707 }
708 }
709 };
710
711 /**
712 * Copy all special tableDrag classes from one row's form elements to a
713 * different one, removing any special classes that the destination row
714 * may have had.
715 */
716 Drupal.tableDrag.prototype.copyDragClasses = function (sourceRow, targetRow, group) {
717 var sourceElement = $('.' + group, sourceRow);
718 var targetElement = $('.' + group, targetRow);
719 if (sourceElement.length && targetElement.length) {
720 targetElement[0].className = sourceElement[0].className;
721 }
722 };
723
724 Drupal.tableDrag.prototype.checkScroll = function (cursorY) {
725 var de = document.documentElement;
726 var b = document.body;
727
728 var windowHeight = this.windowHeight = window.innerHeight || (de.clientHeight && de.clientWidth != 0 ? de.clientHeight : b.offsetHeight);
729 var scrollY = this.scrollY = (document.all ? (!de.scrollTop ? b.scrollTop : de.scrollTop) : (window.pageYOffset ? window.pageYOffset : window.scrollY));
730 var trigger = this.scrollSettings.trigger;
731 var delta = 0;
732
733 // Return a scroll speed relative to the edge of the screen.
734 if (cursorY - scrollY > windowHeight - trigger) {
735 delta = trigger / (windowHeight + scrollY - cursorY);
736 delta = (delta > 0 && delta < trigger) ? delta : trigger;
737 return delta * this.scrollSettings.amount;
738 }
739 else if (cursorY - scrollY < trigger) {
740 delta = trigger / (cursorY - scrollY);
741 delta = (delta > 0 && delta < trigger) ? delta : trigger;
742 return -delta * this.scrollSettings.amount;
743 }
744 };
745
746 Drupal.tableDrag.prototype.setScroll = function (scrollAmount) {
747 var self = this;
748
749 this.scrollInterval = setInterval(function () {
750 // Update the scroll values stored in the object.
751 self.checkScroll(self.currentMouseCoords.y);
752 var aboveTable = self.scrollY > self.table.topY;
753 var belowTable = self.scrollY + self.windowHeight < self.table.bottomY;
754 if (scrollAmount > 0 && belowTable || scrollAmount < 0 && aboveTable) {
755 window.scrollBy(0, scrollAmount);
756 }
757 }, this.scrollSettings.interval);
758 };
759
760 Drupal.tableDrag.prototype.restripeTable = function () {
761 // :even and :odd are reversed because jQuery counts from 0 and
762 // we count from 1, so we're out of sync.
763 // Match immediate children of the parent element to allow nesting.
764 $('> tbody > tr.draggable, > tr.draggable', this.table)
765 .filter(':odd').filter('.odd')
766 .removeClass('odd').addClass('even')
767 .end().end()
768 .filter(':even').filter('.even')
769 .removeClass('even').addClass('odd');
770 };
771
772 /**
773 * Stub function. Allows a custom handler when a row begins dragging.
774 */
775 Drupal.tableDrag.prototype.onDrag = function () {
776 return null;
777 };
778
779 /**
780 * Stub function. Allows a custom handler when a row is dropped.
781 */
782 Drupal.tableDrag.prototype.onDrop = function () {
783 return null;
784 };
785
786 /**
787 * Constructor to make a new object to manipulate a table row.
788 *
789 * @param tableRow
790 * The DOM element for the table row we will be manipulating.
791 * @param method
792 * The method in which this row is being moved. Either 'keyboard' or 'mouse'.
793 * @param indentEnabled
794 * Whether the containing table uses indentations. Used for optimizations.
795 * @param maxDepth
796 * The maximum amount of indentations this row may contain.
797 * @param addClasses
798 * Whether we want to add classes to this row to indicate child relationships.
799 */
800 Drupal.tableDrag.prototype.row = function (tableRow, method, indentEnabled, maxDepth, addClasses) {
801 this.element = tableRow;
802 this.method = method;
803 this.group = [tableRow];
804 this.groupDepth = $('.indentation', tableRow).size();
805 this.changed = false;
806 this.table = $(tableRow).parents('table:first').get(0);
807 this.indentEnabled = indentEnabled;
808 this.maxDepth = maxDepth;
809 this.direction = ''; // Direction the row is being moved.
810
811 if (this.indentEnabled) {
812 this.indents = $('.indentation', tableRow).size();
813 this.children = this.findChildren(addClasses);
814 this.group = $.merge(this.group, this.children);
815 // Find the depth of this entire group.
816 for (var n = 0; n < this.group.length; n++) {
817 this.groupDepth = Math.max($('.indentation', this.group[n]).size(), this.groupDepth);
818 }
819 }
820 };
821
822 /**
823 * Find all children of rowObject by indentation.
824 *
825 * @param addClasses
826 * Whether we want to add classes to this row to indicate child relationships.
827 */
828 Drupal.tableDrag.prototype.row.prototype.findChildren = function (addClasses) {
829 var parentIndentation = this.indents;
830 var currentRow = $(this.element, this.table).next('tr.draggable');
831 var rows = [];
832 var child = 0;
833 while (currentRow.length) {
834 var rowIndentation = $('.indentation', currentRow).length;
835 // A greater indentation indicates this is a child.
836 if (rowIndentation > parentIndentation) {
837 child++;
838 rows.push(currentRow[0]);
839 if (addClasses) {
840 $('.indentation', currentRow).each(function (indentNum) {
841 if (child == 1 && (indentNum == parentIndentation)) {
842 $(this).addClass('tree-child-first');
843 }
844 if (indentNum == parentIndentation) {
845 $(this).addClass('tree-child');
846 }
847 else if (indentNum > parentIndentation) {
848 $(this).addClass('tree-child-horizontal');
849 }
850 });
851 }
852 }
853 else {
854 break;
855 }
856 currentRow = currentRow.next('tr.draggable');
857 }
858 if (addClasses && rows.length) {
859 $('.indentation:nth-child(' + (parentIndentation + 1) + ')', rows[rows.length - 1]).addClass('tree-child-last');
860 }
861 return rows;
862 };
863
864 /**
865 * Ensure that two rows are allowed to be swapped.
866 *
867 * @param row
868 * DOM object for the row being considered for swapping.
869 */
870 Drupal.tableDrag.prototype.row.prototype.isValidSwap = function (row) {
871 if (this.indentEnabled) {
872 var prevRow, nextRow;
873 if (this.direction == 'down') {
874 prevRow = row;
875 nextRow = $(row).next('tr').get(0);
876 }
877 else {
878 prevRow = $(row).prev('tr').get(0);
879 nextRow = row;
880 }
881 this.interval = this.validIndentInterval(prevRow, nextRow);
882
883 // We have an invalid swap if the valid indentations interval is empty.
884 if (this.interval.min > this.interval.max) {
885 return false;
886 }
887 }
888
889 // Do not let an un-draggable first row have anything put before it.
890 if (this.table.tBodies[0].rows[0] == row && $(row).is(':not(.draggable)')) {
891 return false;
892 }
893
894 return true;
895 };
896
897 /**
898 * Perform the swap between two rows.
899 *
900 * @param position
901 * Whether the swap will occur 'before' or 'after' the given row.
902 * @param row
903 * DOM element what will be swapped with the row group.
904 */
905 Drupal.tableDrag.prototype.row.prototype.swap = function (position, row) {
906 Drupal.detachBehaviors(this.group, Drupal.settings, 'move');
907 $(row)[position](this.group);
908 Drupal.attachBehaviors(this.group, Drupal.settings);
909 this.changed = true;
910 this.onSwap(row);
911 };
912
913 /**
914 * Determine the valid indentations interval for the row at a given position
915 * in the table.
916 *
917 * @param prevRow
918 * DOM object for the row before the tested position
919 * (or null for first position in the table).
920 * @param nextRow
921 * DOM object for the row after the tested position
922 * (or null for last position in the table).
923 */
924 Drupal.tableDrag.prototype.row.prototype.validIndentInterval = function (prevRow, nextRow) {
925 var minIndent, maxIndent;
926
927 // Minimum indentation:
928 // Do not orphan the next row.
929 minIndent = nextRow ? $('.indentation', nextRow).size() : 0;
930
931 // Maximum indentation:
932 if (!prevRow || $(this.element).is('.tabledrag-root')) {
933 // Do not indent the first row in the table or 'root' rows..
934 maxIndent = 0;
935 }
936 else {
937 // Do not go deeper than as a child of the previous row.
938 maxIndent = $('.indentation', prevRow).size() + ($(prevRow).is('.tabledrag-leaf') ? 0 : 1);
939 // Limit by the maximum allowed depth for the table.
940 if (this.maxDepth) {
941 maxIndent = Math.min(maxIndent, this.maxDepth - (this.groupDepth - this.indents));
942 }
943 }
944
945 return { 'min': minIndent, 'max': maxIndent };
946 };
947
948 /**
949 * Indent a row within the legal bounds of the table.
950 *
951 * @param indentDiff
952 * The number of additional indentations proposed for the row (can be
953 * positive or negative). This number will be adjusted to nearest valid
954 * indentation level for the row.
955 */
956 Drupal.tableDrag.prototype.row.prototype.indent = function (indentDiff) {
957 // Determine the valid indentations interval if not available yet.
958 if (!this.interval) {
959 prevRow = $(this.element).prev('tr').get(0);
960 nextRow = $(this.group).filter(':last').next('tr').get(0);
961 this.interval = this.validIndentInterval(prevRow, nextRow);
962 }
963
964 // Adjust to the nearest valid indentation.
965 var indent = this.indents + indentDiff;
966 indent = Math.max(indent, this.interval.min);
967 indent = Math.min(indent, this.interval.max);
968 indentDiff = indent - this.indents;
969
970 for (var n = 1; n <= Math.abs(indentDiff); n++) {
971 // Add or remove indentations.
972 if (indentDiff < 0) {
973 $('.indentation:first', this.group).remove();
974 this.indents--;
975 }
976 else {
977 $('td:first', this.group).prepend(Drupal.theme('tableDragIndentation'));
978 this.indents++;
979 }
980 }
981 if (indentDiff) {
982 // Update indentation for this row.
983 this.changed = true;
984 this.groupDepth += indentDiff;
985 this.onIndent();
986 }
987
988 return indentDiff;
989 };
990
991 /**
992 * Find all siblings for a row, either according to its subgroup or indentation.
993 * Note that the passed in row is included in the list of siblings.
994 *
995 * @param settings
996 * The field settings we're using to identify what constitutes a sibling.
997 */
998 Drupal.tableDrag.prototype.row.prototype.findSiblings = function (rowSettings) {
999 var siblings = [];
1000 var directions = ['prev', 'next'];
1001 var rowIndentation = this.indents;
1002 for (var d in directions) {
1003 var checkRow = $(this.element)[directions[d]]();
1004 while (checkRow.length) {
1005 // Check that the sibling contains a similar target field.
1006 if ($('.' + rowSettings.target, checkRow)) {
1007 // Either add immediately if this is a flat table, or check to ensure
1008 // that this row has the same level of indentation.
1009 if (this.indentEnabled) {
1010 var checkRowIndentation = $('.indentation', checkRow).length;
1011 }
1012
1013 if (!(this.indentEnabled) || (checkRowIndentation == rowIndentation)) {
1014 siblings.push(checkRow[0]);
1015 }
1016 else if (checkRowIndentation < rowIndentation) {
1017 // No need to keep looking for siblings when we get to a parent.
1018 break;
1019 }
1020 }
1021 else {
1022 break;
1023 }
1024 checkRow = $(checkRow)[directions[d]]();
1025 }
1026 // Since siblings are added in reverse order for previous, reverse the
1027 // completed list of previous siblings. Add the current row and continue.
1028 if (directions[d] == 'prev') {
1029 siblings.reverse();
1030 siblings.push(this.element);
1031 }
1032 }
1033 return siblings;
1034 };
1035
1036 /**
1037 * Remove indentation helper classes from the current row group.
1038 */
1039 Drupal.tableDrag.prototype.row.prototype.removeIndentClasses = function () {
1040 for (n in this.children) {
1041 $('.indentation', this.children[n])
1042 .removeClass('tree-child')
1043 .removeClass('tree-child-first')
1044 .removeClass('tree-child-last')
1045 .removeClass('tree-child-horizontal');
1046 }
1047 };
1048
1049 /**
1050 * Add an asterisk or other marker to the changed row.
1051 */
1052 Drupal.tableDrag.prototype.row.prototype.markChanged = function () {
1053 var marker = Drupal.theme('tableDragChangedMarker');
1054 var cell = $('td:first', this.element);
1055 if ($('span.tabledrag-changed', cell).length == 0) {
1056 cell.append(marker);
1057 }
1058 };
1059
1060 /**
1061 * Stub function. Allows a custom handler when a row is indented.
1062 */
1063 Drupal.tableDrag.prototype.row.prototype.onIndent = function () {
1064 return null;
1065 };
1066
1067 /**
1068 * Stub function. Allows a custom handler when a row is swapped.
1069 */
1070 Drupal.tableDrag.prototype.row.prototype.onSwap = function (swappedRow) {
1071 return null;