Valid HTML test
[weblabels.fsf.org.git] / defectivebydesign.org / 20120801 / files-orig / misc / tabledrag.js
1 (function ($) {
2
3 /**
4 * Drag and drop table rows with field manipulation.
5 *
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.
9 *
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.
13 */
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]);
21 });
22 }
23 }
24 };
25
26 /**
27 * Constructor for the tableDrag object. Provides table and field manipulation.
28 *
29 * @param table
30 * DOM object for the table to be made draggable.
31 * @param tableSettings
32 * Settings for the table added via drupal_add_dragtable().
33 */
34 Drupal.tableDrag = function (table, tableSettings) {
35 var self = this;
36
37 // Required object variables.
38 this.table = table;
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.
47
48 // Configure the scroll settings.
49 this.scrollSettings = { amount: 4, interval: 50, trigger: 70 };
50 this.scrollInterval = null;
51 this.scrollY = 0;
52 this.windowHeight = 0;
53
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;
62 }
63 if (tableSettings[group][n].limit > 0) {
64 this.maxDepth = tableSettings[group][n].limit;
65 }
66 }
67 }
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;
78 testRow.remove();
79 }
80
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); });
84
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.'))
88 .click(function () {
89 if ($.cookie('Drupal.tableDrag.showWeight') == 1) {
90 self.hideColumns();
91 }
92 else {
93 self.showColumns();
94 }
95 return false;
96 })
97 .wrap('<div class="tabledrag-toggle-weight-wrapper"></div>')
98 .parent()
99 );
100
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..
105 self.initColumns();
106
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); });
111 };
112
113 /**
114 * Initialize columns containing form elements to be hidden by default,
115 * according to the settings for this tableDrag instance.
116 *
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.
120 */
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');
129 break;
130 }
131 }
132
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;
145 }
146 });
147 if (index > 0) {
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');
152 }
153 else {
154 // Mark this cell so we can hide it.
155 cell.addClass('tabledrag-hide');
156 }
157 }
158 });
159 }
160 }
161
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.
168 expires: 365
169 });
170 this.hideColumns();
171 }
172 // Check cookie value and show/hide weight columns accordingly.
173 else {
174 if ($.cookie('Drupal.tableDrag.showWeight') == 1) {
175 this.showColumns();
176 }
177 else {
178 this.hideColumns();
179 }
180 }
181 };
182
183 /**
184 * Hide the columns containing weight/parent form elements.
185 * Undo showColumns().
186 */
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;
195 });
196 // Change link text.
197 $('.tabledrag-toggle-weight').text(Drupal.t('Show row weights'));
198 // Change cookie.
199 $.cookie('Drupal.tableDrag.showWeight', 0, {
200 path: Drupal.settings.basePath,
201 // The cookie expires in one year.
202 expires: 365
203 });
204 };
205
206 /**
207 * Show the columns containing weight/parent form elements
208 * Undo hideColumns().
209 */
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;
218 });
219 // Change link text.
220 $('.tabledrag-toggle-weight').text(Drupal.t('Hide row weights'));
221 // Change cookie.
222 $.cookie('Drupal.tableDrag.showWeight', 1, {
223 path: Drupal.settings.basePath,
224 // The cookie expires in one year.
225 expires: 365
226 });
227 };
228
229 /**
230 * Find the target used within a particular row and group.
231 */
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];
241 }
242 return rowSettings;
243 }
244 }
245 };
246
247 /**
248 * Take an item and add event handlers to make it become draggable.
249 */
250 Drupal.tableDrag.prototype.makeDraggable = function (item) {
251 var self = this;
252
253 // Create the handle.
254 var handle = $('<a href="#" class="tabledrag-handle"><div class="handle">&nbsp;</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);
260 }
261 else {
262 $('td:first', item).prepend(handle);
263 }
264
265 // Add hover action for the handle.
266 handle.hover(function () {
267 self.dragObject == null ? $(this).addClass('tabledrag-handle-hover') : null;
268 }, function () {
269 self.dragObject == null ? $(this).removeClass('tabledrag-handle-hover') : null;
270 });
271
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;
280 }
281
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();
285 }
286
287 // Create a new rowObject for manipulation of this row.
288 self.rowObject = new self.row(item, 'mouse', self.indentEnabled, self.maxDepth, true);
289
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;
293
294 // Add classes to the handle and row.
295 $(this).addClass('tabledrag-handle-hover');
296 $(item).addClass('drag');
297
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');
302 }
303
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');
307 }
308
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;
312
313 // Call optional placeholder function.
314 self.onDrag();
315 return false;
316 });
317
318 // Prevent the anchor tag from jumping us to the top of the page.
319 handle.click(function () {
320 return false;
321 });
322
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;
327 });
328
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);
334 }
335 });
336
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);
342 }
343
344 var keyChange = false;
345 switch (event.keyCode) {
346 case 37: // Left arrow.
347 case 63234: // Safari left arrow.
348 keyChange = true;
349 self.rowObject.indent(-1 * self.rtl);
350 break;
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);
356 }
357 if (previousRow) {
358 self.safeBlur = false; // Do not allow the onBlur cleanup.
359 self.rowObject.direction = 'up';
360 keyChange = true;
361
362 if ($(item).is('.tabledrag-root')) {
363 // Swap with the previous top-level row.
364 var groupHeight = 0;
365 while (previousRow && $('.indentation', previousRow).size()) {
366 previousRow = $(previousRow).prev('tr').get(0);
367 groupHeight += $(previousRow).is(':hidden') ? 0 : previousRow.offsetHeight;
368 }
369 if (previousRow) {
370 self.rowObject.swap('before', previousRow);
371 // No need to check for indentation, 0 is the only valid one.
372 window.scrollBy(0, -groupHeight);
373 }
374 }
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
377 // and undraggable).
378 self.rowObject.swap('before', previousRow);
379 self.rowObject.interval = null;
380 self.rowObject.indent(0);
381 window.scrollBy(0, -parseInt(item.offsetHeight, 10));
382 }
383 handle.get(0).focus(); // Regain focus after the DOM manipulation.
384 }
385 break;
386 case 39: // Right arrow.
387 case 63235: // Safari right arrow.
388 keyChange = true;
389 self.rowObject.indent(1 * self.rtl);
390 break;
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);
396 }
397 if (nextRow) {
398 self.safeBlur = false; // Do not allow the onBlur cleanup.
399 self.rowObject.direction = 'down';
400 keyChange = true;
401
402 if ($(item).is('.tabledrag-root')) {
403 // Swap with the next group (necessarily a top-level one).
404 var groupHeight = 0;
405 nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false);
406 if (nextGroup) {
407 $(nextGroup.group).each(function () {
408 groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight;
409 });
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));
414 }
415 }
416 else {
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));
422 }
423 handle.get(0).focus(); // Regain focus after the DOM manipulation.
424 }
425 break;
426 }
427
428 if (self.rowObject && self.rowObject.changed == true) {
429 $(item).addClass('drag');
430 if (self.oldRowElement) {
431 $(self.oldRowElement).removeClass('drag-previous');
432 }
433 self.oldRowElement = item;
434 self.restripeTable();
435 self.onDrag();
436 }
437
438 // Returning false if we have an arrow key to prevent scrolling.
439 if (keyChange) {
440 return false;
441 }
442 });
443
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.
453 return false;
454 }
455 });
456 };
457
458 /**
459 * Mousemove event handler, bound to document.
460 */
461 Drupal.tableDrag.prototype.dragRow = function (event, self) {
462 if (self.dragObject) {
463 self.currentMouseCoords = self.mouseCoords(event);
464
465 var y = self.currentMouseCoords.y - self.dragObject.initMouseOffset.y;
466 var x = self.currentMouseCoords.x - self.dragObject.initMouseOffset.x;
467
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.
472
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);
480 }
481
482 // If we have a valid target, perform the swap and restripe the table.
483 var currentRow = self.findDropTargetRow(x, y);
484 if (currentRow) {
485 if (self.rowObject.direction == 'down') {
486 self.rowObject.swap('after', currentRow, self);
487 }
488 else {
489 self.rowObject.swap('before', currentRow, self);
490 }
491 self.restripeTable();
492 }
493 }
494
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);
506 }
507
508 return false;
509 }
510 };
511
512 /**
513 * Mouseup event handler, bound to document.
514 * Blur event handler, bound to drag handle for keyboard support.
515 */
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);
524
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);
532 }
533 }
534 }
535
536 self.rowObject.markChanged();
537 if (self.changed == false) {
538 $(Drupal.theme('tableDragChangedWarning')).insertBefore(self.table).hide().fadeIn('slow');
539 self.changed = true;
540 }
541 }
542
543 if (self.indentEnabled) {
544 self.rowObject.removeIndentClasses();
545 }
546 if (self.oldRowElement) {
547 $(self.oldRowElement).removeClass('drag-previous');
548 }
549 $(droppedRow).removeClass('drag').addClass('drag-previous');
550 self.oldRowElement = droppedRow;
551 self.onDrop();
552 self.rowObject = null;
553 }
554
555 // Functionality specific only to mouseup event.
556 if (self.dragObject != null) {
557 $('.tabledrag-handle', droppedRow).removeClass('tabledrag-handle-hover');
558
559 self.dragObject = null;
560 $('body').removeClass('drag');
561 clearInterval(self.scrollInterval);
562
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');
566 }
567 }
568 };
569
570 /**
571 * Get the mouse coordinates from the event (allowing for browser differences).
572 */
573 Drupal.tableDrag.prototype.mouseCoords = function (event) {
574 if (event.pageX || event.pageY) {
575 return { x: event.pageX, y: event.pageY };
576 }
577 return {
578 x: event.clientX + document.body.scrollLeft - document.body.clientLeft,
579 y: event.clientY + document.body.scrollTop - document.body.clientTop
580 };
581 };
582
583 /**
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.
586 */
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 };
591 };
592
593 /**
594 * Find the row the mouse is currently over. This row is then taken and swapped
595 * with the one being dragged.
596 *
597 * @param x
598 * The x coordinate of the mouse on the page (not the screen).
599 * @param y
600 * The y coordinate of the mouse on the page (not the screen).
601 */
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++) {
605 var row = rows[n];
606 var indentDiff = 0;
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;
613 }
614 // Other browsers.
615 else {
616 var rowHeight = parseInt(row.offsetHeight, 10) / 2;
617 }
618
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) {
625 return null;
626 }
627 }
628 }
629 else {
630 // Do not allow a row to be swapped with itself.
631 if (row == this.rowObject.element) {
632 return null;
633 }
634 }
635
636 // Check that swapping with this row is allowed.
637 if (!this.rowObject.isValidSwap(row)) {
638 return null;
639 }
640
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
643 // row.
644 while ($(row).is(':hidden') && $(row).prev('tr').is(':hidden')) {
645 row = $(row).prev('tr').get(0);
646 }
647 return row;
648 }
649 }
650 return null;
651 };
652
653 /**
654 * After the row is dropped, update the table fields according to the settings
655 * set for this table.
656 *
657 * @param changedRow
658 * DOM object for the row that was just dropped.
659 */
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);
665 }
666 };
667
668 /**
669 * After the row is dropped, update a single table field according to specific
670 * settings.
671 *
672 * @param changedRow
673 * DOM object for the row that was just dropped.
674 * @param group
675 * The settings group on which field updates will occur.
676 */
677 Drupal.tableDrag.prototype.updateField = function (changedRow, group) {
678 var rowSettings = this.rowSettings(group, changedRow);
679
680 // Set the row as its own target.
681 if (rowSettings.relationship == 'self' || rowSettings.relationship == 'group') {
682 var sourceRow = changedRow;
683 }
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;
693 }
694 }
695 else {
696 sourceRow = previousRow;
697 }
698 }
699 else if ($(nextRow).is('.draggable') && $('.' + group, nextRow).length) {
700 if (this.indentEnabled) {
701 if ($('.indentations', nextRow).size() == $('.indentations', changedRow)) {
702 sourceRow = nextRow;
703 }
704 }
705 else {
706 sourceRow = nextRow;
707 }
708 }
709 }
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');
716 }
717 // If we found a row.
718 if (previousRow.length) {
719 sourceRow = previousRow[0];
720 }
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.
723 else {
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);
730 }
731 var useSibling = true;
732 }
733 }
734
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);
739
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.
742 if (useSibling) {
743 rowSettings.relationship = 'sibling';
744 rowSettings.source = rowSettings.target;
745 }
746
747 var targetClass = '.' + rowSettings.target;
748 var targetElement = $(targetClass, changedRow).get(0);
749
750 // Check if a target element exists in this row.
751 if (targetElement) {
752 var sourceClass = '.' + rowSettings.source;
753 var sourceElement = $(sourceClass, sourceRow).get(0);
754 switch (rowSettings.action) {
755 case 'depth':
756 // Get the depth of the target row.
757 targetElement.value = $('.indentation', $(sourceElement).parents('tr:first')).size();
758 break;
759 case 'match':
760 // Update the value.
761 targetElement.value = sourceElement.value;
762 break;
763 case 'order':
764 var siblings = this.rowObject.findSiblings(rowSettings);
765 if ($(targetElement).is('select')) {
766 // Get a list of acceptable values.
767 var values = [];
768 $('option', targetElement).each(function () {
769 values.push(this.value);
770 });
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();
777 }
778 else {
779 this.value = maxVal;
780 }
781 });
782 }
783 else {
784 // Assume a numeric input field.
785 var weight = parseInt($(targetClass, siblings[0]).val(), 10) || 0;
786 $(targetClass, siblings).each(function () {
787 this.value = weight;
788 weight++;
789 });
790 }
791 break;
792 }
793 }
794 };
795
796 /**
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
799 * may have had.
800 */
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;
806 }
807 };
808
809 Drupal.tableDrag.prototype.checkScroll = function (cursorY) {
810 var de = document.documentElement;
811 var b = document.body;
812
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;
816 var delta = 0;
817
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;
823 }
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;
828 }
829 };
830
831 Drupal.tableDrag.prototype.setScroll = function (scrollAmount) {
832 var self = this;
833
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);
841 }
842 }, this.scrollSettings.interval);
843 };
844
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');
853 };
854
855 /**
856 * Stub function. Allows a custom handler when a row begins dragging.
857 */
858 Drupal.tableDrag.prototype.onDrag = function () {
859 return null;
860 };
861
862 /**
863 * Stub function. Allows a custom handler when a row is dropped.
864 */
865 Drupal.tableDrag.prototype.onDrop = function () {
866 return null;
867 };
868
869 /**
870 * Constructor to make a new object to manipulate a table row.
871 *
872 * @param tableRow
873 * The DOM element for the table row we will be manipulating.
874 * @param method
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.
878 * @param maxDepth
879 * The maximum amount of indentations this row may contain.
880 * @param addClasses
881 * Whether we want to add classes to this row to indicate child relationships.
882 */
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.
893
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);
901 }
902 }
903 };
904
905 /**
906 * Find all children of rowObject by indentation.
907 *
908 * @param addClasses
909 * Whether we want to add classes to this row to indicate child relationships.
910 */
911 Drupal.tableDrag.prototype.row.prototype.findChildren = function (addClasses) {
912 var parentIndentation = this.indents;
913 var currentRow = $(this.element, this.table).next('tr.draggable');
914 var rows = [];
915 var child = 0;
916 while (currentRow.length) {
917 var rowIndentation = $('.indentation', currentRow).length;
918 // A greater indentation indicates this is a child.
919 if (rowIndentation > parentIndentation) {
920 child++;
921 rows.push(currentRow[0]);
922 if (addClasses) {
923 $('.indentation', currentRow).each(function (indentNum) {
924 if (child == 1 && (indentNum == parentIndentation)) {
925 $(this).addClass('tree-child-first');
926 }
927 if (indentNum == parentIndentation) {
928 $(this).addClass('tree-child');
929 }
930 else if (indentNum > parentIndentation) {
931 $(this).addClass('tree-child-horizontal');
932 }
933 });
934 }
935 }
936 else {
937 break;
938 }
939 currentRow = currentRow.next('tr.draggable');
940 }
941 if (addClasses && rows.length) {
942 $('.indentation:nth-child(' + (parentIndentation + 1) + ')', rows[rows.length - 1]).addClass('tree-child-last');
943 }
944 return rows;
945 };
946
947 /**
948 * Ensure that two rows are allowed to be swapped.
949 *
950 * @param row
951 * DOM object for the row being considered for swapping.
952 */
953 Drupal.tableDrag.prototype.row.prototype.isValidSwap = function (row) {
954 if (this.indentEnabled) {
955 var prevRow, nextRow;
956 if (this.direction == 'down') {
957 prevRow = row;
958 nextRow = $(row).next('tr').get(0);
959 }
960 else {
961 prevRow = $(row).prev('tr').get(0);
962 nextRow = row;
963 }
964 this.interval = this.validIndentInterval(prevRow, nextRow);
965
966 // We have an invalid swap if the valid indentations interval is empty.
967 if (this.interval.min > this.interval.max) {
968 return false;
969 }
970 }
971
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)')) {
974 return false;
975 }
976
977 return true;
978 };
979
980 /**
981 * Perform the swap between two rows.
982 *
983 * @param position
984 * Whether the swap will occur 'before' or 'after' the given row.
985 * @param row
986 * DOM element what will be swapped with the row group.
987 */
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);
992 this.changed = true;
993 this.onSwap(row);
994 };
995
996 /**
997 * Determine the valid indentations interval for the row at a given position
998 * in the table.
999 *
1000 * @param prevRow
1001 * DOM object for the row before the tested position
1002 * (or null for first position in the table).
1003 * @param nextRow
1004 * DOM object for the row after the tested position
1005 * (or null for last position in the table).
1006 */
1007 Drupal.tableDrag.prototype.row.prototype.validIndentInterval = function (prevRow, nextRow) {
1008 var minIndent, maxIndent;
1009
1010 // Minimum indentation:
1011 // Do not orphan the next row.
1012 minIndent = nextRow ? $('.indentation', nextRow).size() : 0;
1013
1014 // Maximum indentation:
1015 if (!prevRow || $(prevRow).is(':not(.draggable)') || $(this.element).is('.tabledrag-root')) {
1016 // Do not indent:
1017 // - the first row in the table,
1018 // - rows dragged below a non-draggable row,
1019 // - 'root' rows.
1020 maxIndent = 0;
1021 }
1022 else {
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));
1028 }
1029 }
1030
1031 return { 'min': minIndent, 'max': maxIndent };
1032 };
1033
1034 /**
1035 * Indent a row within the legal bounds of the table.
1036 *
1037 * @param indentDiff
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.
1041 */
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);
1048 }
1049
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;
1055
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();
1060 this.indents--;
1061 }
1062 else {
1063 $('td:first', this.group).prepend(Drupal.theme('tableDragIndentation'));
1064 this.indents++;
1065 }
1066 }
1067 if (indentDiff) {
1068 // Update indentation for this row.
1069 this.changed = true;
1070 this.groupDepth += indentDiff;
1071 this.onIndent();
1072 }
1073
1074 return indentDiff;
1075 };
1076
1077 /**
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.
1080 *
1081 * @param settings
1082 * The field settings we're using to identify what constitutes a sibling.
1083 */
1084 Drupal.tableDrag.prototype.row.prototype.findSiblings = function (rowSettings) {
1085 var siblings = [];
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;
1097 }
1098
1099 if (!(this.indentEnabled) || (checkRowIndentation == rowIndentation)) {
1100 siblings.push(checkRow[0]);
1101 }
1102 else if (checkRowIndentation < rowIndentation) {
1103 // No need to keep looking for siblings when we get to a parent.
1104 break;
1105 }
1106 }
1107 else {
1108 break;
1109 }
1110 checkRow = $(checkRow)[directions[d]]();
1111 }
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') {
1115 siblings.reverse();
1116 siblings.push(this.element);
1117 }
1118 }
1119 return siblings;
1120 };
1121
1122 /**
1123 * Remove indentation helper classes from the current row group.
1124 */
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');
1132 }
1133 };
1134
1135 /**
1136 * Add an asterisk or other marker to the changed row.
1137 */
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);
1143 }
1144 };
1145
1146 /**
1147 * Stub function. Allows a custom handler when a row is indented.
1148 */
1149 Drupal.tableDrag.prototype.row.prototype.onIndent = function () {
1150 return null;
1151 };
1152
1153 /**
1154 * Stub function. Allows a custom handler when a row is swapped.
1155 */
1156 Drupal.tableDrag.prototype.row.prototype.onSwap = function (swappedRow) {
1157 return null;
1158 };
1159
1160 Drupal.theme.prototype.tableDragChangedMarker = function () {
1161 return '<span class="warning tabledrag-changed">*</span>';
1162 };
1163
1164 Drupal.theme.prototype.tableDragIndentation = function () {
1165 return '<div class="indentation">&nbsp;</div>';
1166 };
1167
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>';
1170 };
1171
1172 })(jQuery);