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 */ 14Drupal.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 */ 34Drupal.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 pointermove', function (event) { return self.dragRow(event, self); }); 110 $(document).bind('mouseup pointerup', function (event) { return self.dropRow(event, self); }); 111 $(document).bind('touchmove', function (event) { return self.dragRow(event.originalEvent.touches[0], self); }); 112 $(document).bind('touchend', function (event) { return self.dropRow(event.originalEvent.touches[0], self); }); 113}; 114 115/** 116 * Initialize columns containing form elements to be hidden by default, 117 * according to the settings for this tableDrag instance. 118 * 119 * Identify and mark each cell with a CSS class so we can easily toggle 120 * show/hide it. Finally, hide columns if user does not have a 121 * 'Drupal.tableDrag.showWeight' cookie. 122 */ 123Drupal.tableDrag.prototype.initColumns = function () { 124 for (var group in this.tableSettings) { 125 // Find the first field in this group. 126 for (var d in this.tableSettings[group]) { 127 var field = $('.' + this.tableSettings[group][d].target + ':first', this.table); 128 if (field.length && this.tableSettings[group][d].hidden) { 129 var hidden = this.tableSettings[group][d].hidden; 130 var cell = field.closest('td'); 131 break; 132 } 133 } 134 135 // Mark the column containing this field so it can be hidden. 136 if (hidden && cell[0]) {
137 // Add 1 to our indexes. The nth-child selector is 1 based, not 0 based. 138 // Match immediate children of the parent element to allow nesting. 139 var columnIndex = $('> td', cell.parent()).index(cell.get(0)) + 1; 140 $('> thead > tr, > tbody > tr, > tr', this.table).each(function () { 141 // Get the columnIndex and adjust for any colspans in this row. 142 var index = columnIndex; 143 var cells = $(this).children(); 144 cells.each(function (n) { 145 if (n < index && this.colSpan && this.colSpan > 1) { 146 index -= this.colSpan - 1; 147 } 148 }); 149 if (index > 0) { 150 cell = cells.filter(':nth-child(' + index + ')'); 151 if (cell[0].colSpan && cell[0].colSpan > 1) { 152 // If this cell has a colspan, mark it so we can reduce the colspan. 153 cell.addClass('tabledrag-has-colspan'); 154 } 155 else { 156 // Mark this cell so we can hide it. 157 cell.addClass('tabledrag-hide'); 158 } 159 } 160 }); 161 } 162 } 163 164 // Now hide cells and reduce colspans unless cookie indicates previous choice. 165 // Set a cookie if it is not already present. 166 if ($.cookie('Drupal.tableDrag.showWeight') === null) { 167 $.cookie('Drupal.tableDrag.showWeight', 0, { 168 path: Drupal.settings.basePath, 169 // The cookie expires in one year. 170 expires: 365 171 }); 172 this.hideColumns(); 173 } 174 // Check cookie value and show/hide weight columns accordingly. 175 else { 176 if ($.cookie('Drupal.tableDrag.showWeight') == 1) { 177 this.showColumns(); 178 } 179 else { 180 this.hideColumns(); 181 } 182 } 183}; 184 185/** 186 * Hide the columns containing weight/parent form elements. 187 * Undo showColumns(). 188 */ 189Drupal.tableDrag.prototype.hideColumns = function () { 190 // Hide weight/parent cells and headers. 191 $('.tabledrag-hide', 'table.tabledrag-processed').css('display', 'none'); 192 // Show TableDrag handles. 193 $('.tabledrag-handle', 'table.tabledrag-processed').css('display', ''); 194 // Reduce the colspan of any effected multi-span columns. 195 $('.tabledrag-has-colspan', 'table.tabledrag-processed').each(function () { 196 this.colSpan = this.colSpan - 1; 197 }); 198 // Change link text. 199 $('.tabledrag-toggle-weight').text(Drupal.t('Show row weights')); 200 // Change cookie. 201 $.cookie('Drupal.tableDrag.showWeight', 0, { 202 path: Drupal.settings.basePath, 203 // The cookie expires in one year. 204 expires: 365 205 });
206 // Trigger an event to allow other scripts to react to this display change. 207 $('table.tabledrag-processed').trigger('columnschange', 'hide'); 208}; 209 210/** 211 * Show the columns containing weight/parent form elements 212 * Undo hideColumns(). 213 */ 214Drupal.tableDrag.prototype.showColumns = function () { 215 // Show weight/parent cells and headers. 216 $('.tabledrag-hide', 'table.tabledrag-processed').css('display', ''); 217 // Hide TableDrag handles. 218 $('.tabledrag-handle', 'table.tabledrag-processed').css('display', 'none'); 219 // Increase the colspan for any columns where it was previously reduced. 220 $('.tabledrag-has-colspan', 'table.tabledrag-processed').each(function () { 221 this.colSpan = this.colSpan + 1; 222 }); 223 // Change link text. 224 $('.tabledrag-toggle-weight').text(Drupal.t('Hide row weights')); 225 // Change cookie. 226 $.cookie('Drupal.tableDrag.showWeight', 1, { 227 path: Drupal.settings.basePath, 228 // The cookie expires in one year. 229 expires: 365 230 }); 231 // Trigger an event to allow other scripts to react to this display change. 232 $('table.tabledrag-processed').trigger('columnschange', 'show'); 233}; 234 235/** 236 * Find the target used within a particular row and group. 237 */ 238Drupal.tableDrag.prototype.rowSettings = function (group, row) { 239 var field = $('.' + group, row); 240 for (var delta in this.tableSettings[group]) { 241 var targetClass = this.tableSettings[group][delta].target; 242 if (field.is('.' + targetClass)) { 243 // Return a copy of the row settings. 244 var rowSettings = {}; 245 for (var n in this.tableSettings[group][delta]) { 246 rowSettings[n] = this.tableSettings[group][delta][n]; 247 } 248 return rowSettings; 249 } 250 } 251}; 252 253/** 254 * Take an item and add event handlers to make it become draggable. 255 */ 256Drupal.tableDrag.prototype.makeDraggable = function (item) { 257 var self = this; 258 259 // Create the handle. 260 var handle = $('<a href="#" class="tabledrag-handle"><div class="handle"> </div></a>').attr('title', Drupal.t('Drag to re-order')); 261 // Insert the handle after indentations (if any). 262 if ($('td:first .indentation:last', item).length) { 263 $('td:first .indentation:last', item).after(handle); 264 // Update the total width of indentation in this entire table. 265 self.indentCount = Math.max($('.indentation', item).length, self.indentCount); 266 } 267 else { 268 $('td:first', item).prepend(handle); 269 } 270 271 // Add hover action for the handle. 272 handle.hover(function () { 273 self.dragObject == null ? $(this).addClass('tabledrag-handle-hover') : null; 274 }, function () { 275 self.dragObject == null ? $(this).removeClass('tabledrag-handle-hover') : null; 276 }); 277 278 // Add the mousedown action for the handle. 279 handle.bind('mousedown touchstart pointerdown', function (event) { 280 if (event.originalEvent.type == "touchstart") { 281 event = event.originalEvent.touches[0]; 282 } 283 // Create a new dragObject recording the event information. 284 self.dragObject = {}; 285 self.dragObject.initMouseOffset = self.getMouseOffset(item, event); 286 self.dragObject.initMouseCoords = self.mouseCoords(event); 287 if (self.indentEnabled) { 288 self.dragObject.indentMousePos = self.dragObject.initMouseCoords; 289 } 290 291 // If there's a lingering row object from the keyboard, remove its focus. 292 if (self.rowObject) { 293 $('a.tabledrag-handle', self.rowObject.element).blur(); 294 } 295 296 // Create a new rowObject for manipulation of this row. 297 self.rowObject = new self.row(item, 'mouse', self.indentEnabled, self.maxDepth, true); 298 299 // Save the position of the table. 300 self.table.topY = $(self.table).offset().top; 301 self.table.bottomY = self.table.topY + self.table.offsetHeight; 302 303 // Add classes to the handle and row. 304 $(this).addClass('tabledrag-handle-hover'); 305 $(item).addClass('drag'); 306 307 // Set the document to use the move cursor during drag. 308 $('body').addClass('drag'); 309 if (self.oldRowElement) { 310 $(self.oldRowElement).removeClass('drag-previous'); 311 } 312 313 // Hack for IE6 that flickers uncontrollably if select lists are moved. 314 if (navigator.userAgent.indexOf('MSIE 6.') != -1) { 315 $('select', this.table).css('display', 'none'); 316 } 317 318 // Hack for Konqueror, prevent the blur handler from firing. 319 // Konqueror always gives links focus, even after returning false on mousedown. 320 self.safeBlur = false;
321 322 // Call optional placeholder function. 323 self.onDrag(); 324 return false; 325 }); 326 327 // Prevent the anchor tag from jumping us to the top of the page. 328 handle.click(function () { 329 return false; 330 }); 331 332 // Similar to the hover event, add a class when the handle is focused. 333 handle.focus(function () { 334 $(this).addClass('tabledrag-handle-hover'); 335 self.safeBlur = true; 336 }); 337 338 // Remove the handle class on blur and fire the same function as a mouseup. 339 handle.blur(function (event) { 340 $(this).removeClass('tabledrag-handle-hover'); 341 if (self.rowObject && self.safeBlur) { 342 self.dropRow(event, self); 343 } 344 }); 345 346 // Add arrow-key support to the handle. 347 handle.keydown(function (event) { 348 // If a rowObject doesn't yet exist and this isn't the tab key. 349 if (event.keyCode != 9 && !self.rowObject) { 350 self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true); 351 } 352 353 var keyChange = false; 354 switch (event.keyCode) { 355 case 37: // Left arrow. 356 case 63234: // Safari left arrow. 357 keyChange = true; 358 self.rowObject.indent(-1 * self.rtl); 359 break; 360 case 38: // Up arrow. 361 case 63232: // Safari up arrow. 362 var previousRow = $(self.rowObject.element).prev('tr').get(0); 363 while (previousRow && $(previousRow).is(':hidden')) { 364 previousRow = $(previousRow).prev('tr').get(0); 365 } 366 if (previousRow) { 367 self.safeBlur = false;
367 // Do not allow the onBlur cleanup. 368 self.rowObject.direction = 'up'; 369 keyChange = true; 370 371 if ($(item).is('.tabledrag-root')) { 372 // Swap with the previous top-level row. 373 var groupHeight = 0; 374 while (previousRow && $('.indentation', previousRow).length) { 375 previousRow = $(previousRow).prev('tr').get(0); 376 groupHeight += $(previousRow).is(':hidden') ? 0 : previousRow.offsetHeight; 377 } 378 if (previousRow) { 379 self.rowObject.swap('before', previousRow); 380 // No need to check for indentation, 0 is the only valid one. 381 window.scrollBy(0, -groupHeight); 382 } 383 } 384 else if (self.table.tBodies[0].rows[0] != previousRow || $(previousRow).is('.draggable')) { 385 // Swap with the previous row (unless previous row is the first one 386 // and undraggable). 387 self.rowObject.swap('before', previousRow); 388 self.rowObject.interval = null; 389 self.rowObject.indent(0); 390 window.scrollBy(0, -parseInt(item.offsetHeight, 10)); 391 } 392 handle.get(0).focus(); // Regain focus after the DOM manipulation. 393 } 394 break; 395 case 39: // Right arrow. 396 case 63235: // Safari right arrow. 397 keyChange = true; 398 self.rowObject.indent(1 * self.rtl); 399 break; 400 case 40: // Down arrow. 401 case 63233: // Safari down arrow. 402 var nextRow = $(self.rowObject.group).filter(':last').next('tr').get(0); 403 while (nextRow && $(nextRow).is(':hidden')) { 404 nextRow = $(nextRow).next('tr').get(0); 405 } 406 if (nextRow) { 407 self.safeBlur = false;
407 // Do not allow the onBlur cleanup. 408 self.rowObject.direction = 'down'; 409 keyChange = true; 410 411 if ($(item).is('.tabledrag-root')) { 412 // Swap with the next group (necessarily a top-level one). 413 var groupHeight = 0; 414 var nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false); 415 if (nextGroup) { 416 $(nextGroup.group).each(function () { 417 groupHeight += $(this).is(':hidden') ? 0 : this.offsetHeight; 418 }); 419 var nextGroupRow = $(nextGroup.group).filter(':last').get(0); 420 self.rowObject.swap('after', nextGroupRow); 421 // No need to check for indentation, 0 is the only valid one. 422 window.scrollBy(0, parseInt(groupHeight, 10)); 423 } 424 } 425 else { 426 // Swap with the next row. 427 self.rowObject.swap('after', nextRow); 428 self.rowObject.interval = null; 429 self.rowObject.indent(0); 430 window.scrollBy(0, parseInt(item.offsetHeight, 10)); 431 } 432 handle.get(0).focus(); // Regain focus after the DOM manipulation. 433 } 434 break; 435 } 436 437 if (self.rowObject && self.rowObject.changed == true) { 438 $(item).addClass('drag'); 439 if (self.oldRowElement) { 440 $(self.oldRowElement).removeClass('drag-previous'); 441 } 442 self.oldRowElement = item; 443 self.restripeTable(); 444 self.onDrag(); 445 } 446 447 // Returning false if we have an arrow key to prevent scrolling. 448 if (keyChange) { 449 return false; 450 } 451 }); 452 453 // Compatibility addition, return false on keypress to prevent unwanted scrolling. 454 // IE and Safari will suppress scrolling on keydown, but all other browsers 455 // need to return false on keypress. http://www.quirksmode.org/js/keys.html 456 handle.keypress(function (event) { 457 switch (event.keyCode) { 458 case 37: // Left arrow. 459 case 38: // Up arrow. 460 case 39: // Right arrow. 461 case 40: // Down arrow. 462 return false; 463 } 464 }); 465}; 466 467/** 468 * Mousemove event handler, bound to document. 469 */ 470Drupal.tableDrag.prototype.dragRow = function (event, self) { 471 if (self.dragObject) { 472 self.currentMouseCoords = self.mouseCoords(event); 473 474 var y = self.currentMouseCoords.y - self.dragObject.initMouseOffset.y; 475 var x = self.currentMouseCoords.x - self.dragObject.initMouseOffset.x; 476 477 // Check for row swapping and vertical scrolling. 478 if (y != self.oldY) { 479 self.rowObject.direction = y > self.oldY ? 'down' : 'up'; 480 self.oldY = y; // Update the old value. 481 482 // Check if the window should be scrolled (and how fast). 483 var scrollAmount = self.checkScroll(self.currentMouseCoords.y); 484 // Stop any current scrolling. 485 clearInterval(self.scrollInterval); 486 // Continue scrolling if the mouse has moved in the scroll direction. 487 if (scrollAmount > 0 && self.rowObject.direction == 'down' || scrollAmount < 0 && self.rowObject.direction == 'up') { 488 self.setScroll(scrollAmount); 489 } 490 491 // If we have a valid target, perform the swap and restripe the table. 492 var currentRow = self.findDropTargetRow(x, y); 493 if (currentRow) { 494 if (self.rowObject.direction == 'down') { 495 self.rowObject.swap('after', currentRow, self); 496 } 497 else { 498 self.rowObject.swap('before', currentRow, self); 499 } 500 self.restripeTable(); 501 } 502 } 503 504 // Similar to row swapping, handle indentations. 505 if (self.indentEnabled) { 506 var xDiff = self.currentMouseCoords.x - self.dragObject.indentMousePos.x; 507 // Set the number of indentations the mouse has been moved left or right. 508 var indentDiff = Math.round(xDiff / self.indentAmount); 509 // Indent the row with our estimated diff, which may be further 510 // restricted according to the rows around this row. 511 var indentChange = self.rowObject.indent(indentDiff); 512 // Update table and mouse indentations. 513 self.dragObject.indentMousePos.x += self.indentAmount * indentChange * self.rtl; 514 self.indentCount = Math.max(self.indentCount, self.rowObject.indents); 515 } 516 517 return false;
518 } 519}; 520 521/** 522 * Mouseup event handler, bound to document. 523 * Blur event handler, bound to drag handle for keyboard support. 524 */ 525Drupal.tableDrag.prototype.dropRow = function (event, self) { 526 // Drop row functionality shared between mouseup and blur events. 527 if (self.rowObject != null) { 528 var droppedRow = self.rowObject.element; 529 // The row is already in the right place so we just release it. 530 if (self.rowObject.changed == true) { 531 // Update the fields in the dropped row. 532 self.updateFields(droppedRow); 533 534 // If a setting exists for affecting the entire group, update all the 535 // fields in the entire dragged group. 536 for (var group in self.tableSettings) { 537 var rowSettings = self.rowSettings(group, droppedRow); 538 if (rowSettings.relationship == 'group') { 539 for (var n in self.rowObject.children) { 540 self.updateField(self.rowObject.children[n], group); 541 } 542 } 543 } 544 545 self.rowObject.markChanged(); 546 if (self.changed == false) { 547 $(Drupal.theme('tableDragChangedWarning')).insertBefore(self.table).hide().fadeIn('slow'); 548 self.changed = true; 549 } 550 } 551 552 if (self.indentEnabled) { 553 self.rowObject.removeIndentClasses(); 554 } 555 if (self.oldRowElement) { 556 $(self.oldRowElement).removeClass('drag-previous'); 557 } 558 $(droppedRow).removeClass('drag').addClass('drag-previous'); 559 self.oldRowElement = droppedRow; 560 self.onDrop(); 561 self.rowObject = null; 562 } 563 564 // Functionality specific only to mouseup event. 565 if (self.dragObject != null) { 566 $('.tabledrag-handle', droppedRow).removeClass('tabledrag-handle-hover'); 567 568 self.dragObject = null; 569 $('body').removeClass('drag'); 570 clearInterval(self.scrollInterval); 571 572 // Hack for IE6 that flickers uncontrollably if select lists are moved. 573 if (navigator.userAgent.indexOf('MSIE 6.') != -1) { 574 $('select', this.table).css('display', 'block'); 575 } 576 } 577}; 578 579/** 580 * Get the mouse coordinates from the event (allowing for browser differences). 581 */ 582Drupal.tableDrag.prototype.mouseCoords = function (event) { 583 584 // Match both null and undefined, but not zero, by using != null. 585 // See https://stackoverflow.com/questions/2647867/how-to-determine-if-variable-is-undefined-or-null 586 if (event.pageX != null && event.pageY != null) { 587 return {x: event.pageX, y: event.pageY}; 588 } 589 590 // Complete support for pointer events was only introduced to jQuery in 591 // version 1.11.1; between versions 1.7 and 1.11.0 pointer events have the 592 // pageX and pageY properties undefined. In those cases, the properties must 593 // be retrieved from the event.originalEvent object instead. 594 if (event.originalEvent && event.originalEvent.pageX != null && event.originalEvent.pageY != null) { 595 return {x: event.originalEvent.pageX, y: event.originalEvent.pageY}; 596 } 597 598 // Some old browsers do not support MouseEvent.pageX and *.pageY at all. 599 // See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageY 600 // For those, we look at event.clientX and event.clientY instead. 601 if (event.clientX == null || event.clientY == null) { 602 // In some jQuery versions, some events created by jQuery do not have 603 // clientX and clientY. But the original event might have. 604 if (!event.originalEvent) { 605 throw new Error("The event has no coordinates, and no event.originalEvent."); 606 } 607 event = event.originalEvent; 608 if (event.clientX == null || event.clientY == null) { 609 throw new Error("The original event has no coordinates."); 610 } 611 } 612 613 // Copied from jQuery.event.fix() in jQuery 1.4.1. 614 // In newer jQuery versions, this code is in jQuery.event.mouseHooks.filter(). 615 var doc = document.documentElement, body = document.body; 616 var pageX = event.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); 617 var pageY = event.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); 618 619 return {x: pageX, y: pageY}; 620}; 621 622/** 623 * Given a target element and a mouse event, get the mouse offset from that 624 * element. To do this we need the element's position and the mouse position. 625 */ 626Drupal.tableDrag.prototype.getMouseOffset = function (target, event) { 627 var docPos = $(target).offset(); 628 var mousePos = this.mouseCoords(event); 629 return { x: mousePos.x - docPos.left, y: mousePos.y - docPos.top }; 630}; 631 632/** 633 * Find the row the mouse is currently over. This row is then taken and swapped 634 * with the one being dragged. 635 * 636 * @param x 637 * The x coordinate of the mouse on the page (not the screen). 638 * @param y 639 * The y coordinate of the mouse on the page (not the screen). 640 */ 641Drupal.tableDrag.prototype.findDropTargetRow = function (x, y) { 642 var rows = $(this.table.tBodies[0].rows).not(':hidden'); 643 for (var n = 0; n < rows.length; n++) { 644 var row = rows[n]; 645 var indentDiff = 0; 646 var rowY = $(row).offset().top; 647 // Because Safari does not report offsetHeight on table rows, but does on 648 // table cells, grab the firstChild of the row and use that instead. 649 // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari. 650 if (row.offsetHeight == 0) { 651 var rowHeight = parseInt(row.firstChild.offsetHeight, 10) / 2; 652 } 653 // Other browsers. 654 else { 655 var rowHeight = parseInt(row.offsetHeight, 10) / 2; 656 } 657 658 // Because we always insert before, we need to offset the height a bit. 659 if ((y > (rowY - rowHeight)) && (y < (rowY + rowHeight))) { 660 if (this.indentEnabled) { 661 // Check that this row is not a child of the row being dragged. 662 for (var n in this.rowObject.group) { 663 if (this.rowObject.group[n] == row) { 664 return null; 665 } 666 } 667 } 668 else {
669 // Do not allow a row to be swapped with itself. 670 if (row == this.rowObject.element) { 671 return null; 672 } 673 } 674 675 // Check that swapping with this row is allowed. 676 if (!this.rowObject.isValidSwap(row)) { 677 return null; 678 } 679 680 // We may have found the row the mouse just passed over, but it doesn't 681 // take into account hidden rows. Skip backwards until we find a draggable 682 // row. 683 while ($(row).is(':hidden') && $(row).prev('tr').is(':hidden')) { 684 row = $(row).prev('tr').get(0); 685 } 686 return row; 687 } 688 } 689 return null; 690}; 691 692/** 693 * After the row is dropped, update the table fields according to the settings 694 * set for this table. 695 * 696 * @param changedRow 697 * DOM object for the row that was just dropped. 698 */ 699Drupal.tableDrag.prototype.updateFields = function (changedRow) { 700 for (var group in this.tableSettings) { 701 // Each group may have a different setting for relationship, so we find 702 // the source rows for each separately. 703 this.updateField(changedRow, group); 704 } 705}; 706 707/** 708 * After the row is dropped, update a single table field according to specific 709 * settings. 710 * 711 * @param changedRow 712 * DOM object for the row that was just dropped. 713 * @param group 714 * The settings group on which field updates will occur. 715 */ 716Drupal.tableDrag.prototype.updateField = function (changedRow, group) { 717 var rowSettings = this.rowSettings(group, changedRow); 718 719 // Set the row as its own target. 720 if (rowSettings.relationship == 'self' || rowSettings.relationship == 'group') { 721 var sourceRow = changedRow; 722 } 723 // Siblings are easy, check previous and next rows. 724 else if (rowSettings.relationship == 'sibling') { 725 var previousRow = $(changedRow).prev('tr').get(0); 726 var nextRow = $(changedRow).next('tr').get(0); 727 var sourceRow = changedRow; 728 if ($(previousRow).is('.draggable') && $('.' + group, previousRow).length) { 729 if (this.indentEnabled) { 730 if ($('.indentations', previousRow).length == $('.indentations', changedRow)) { 731 sourceRow = previousRow; 732 } 733 } 734 else { 735 sourceRow = previousRow; 736 } 737 } 738 else if ($(nextRow).is('.draggable') && $('.' + group, nextRow).length) { 739 if (this.indentEnabled) { 740 if ($('.indentations', nextRow).length == $('.indentations', changedRow)) { 741 sourceRow = nextRow; 742 } 743 } 744 else { 745 sourceRow = nextRow; 746 } 747 } 748 } 749 // Parents, look up the tree until we find a field not in this group. 750 // Go up as many parents as indentations in the changed row. 751 else if (rowSettings.relationship == 'parent') { 752 var previousRow = $(changedRow).prev('tr'); 753 while (previousRow.length && $('.indentation', previousRow).length >= this.rowObject.indents) { 754 previousRow = previousRow.prev('tr'); 755 } 756 // If we found a row. 757 if (previousRow.length) { 758 sourceRow = previousRow[0]; 759 } 760 // Otherwise we went all the way to the left of the table without finding 761 // a parent, meaning this item has been placed at the root level. 762 else { 763 // Use the first row in the table as source, because it's guaranteed to 764 // be at the root level. Find the first item, then compare this row 765 // against it as a sibling. 766 sourceRow = $(this.table).find('tr.draggable:first').get(0); 767 if (sourceRow == this.rowObject.element) { 768 sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0); 769 } 770 var useSibling = true; 771 } 772 } 773 774 // Because we may have moved the row from one category to another, 775 // take a look at our sibling and borrow its sources and targets. 776 this.copyDragClasses(sourceRow, changedRow, group); 777 rowSettings = this.rowSettings(group, changedRow); 778 779 // In the case that we're looking for a parent, but the row is at the top 780 // of the tree, copy our sibling's values. 781 if (useSibling) { 782 rowSettings.relationship = 'sibling'; 783 rowSettings.source = rowSettings.target; 784 } 785 786 var targetClass = '.' + rowSettings.target; 787 var targetElement = $(targetClass, changedRow).get(0); 788 789 // Check if a target element exists in this row. 790 if (targetElement) { 791 var sourceClass = '.' + rowSettings.source; 792 var sourceElement = $(sourceClass, sourceRow).get(0); 793 switch (rowSettings.action) { 794 case 'depth': 795 // Get the depth of the target row. 796 targetElement.value = $('.indentation', $(sourceElement).closest('tr')).length; 797 break; 798 case 'match': 799 // Update the value. 800 targetElement.value = sourceElement.value; 801 break; 802 case 'order': 803 var siblings = this.rowObject.findSiblings(rowSettings); 804 if ($(targetElement).is('select')) { 805 // Get a list of acceptable values. 806 var values = []; 807 $('option', targetElement).each(function () { 808 values.push(this.value); 809 }); 810 var maxVal = values[values.length - 1]; 811 // Populate the values in the siblings. 812 $(targetClass, siblings).each(function () { 813 // If there are more items than possible values, assign the maximum value to the row. 814 if (values.length > 0) { 815 this.value = values.shift(); 816 } 817 else { 818 this.value = maxVal; 819 } 820 }); 821 } 822 else { 823 // Assume a numeric input field. 824 var weight = parseInt($(targetClass, siblings[0]).val(), 10) || 0; 825 $(targetClass, siblings).each(function () { 826 this.value = weight; 827 weight++; 828 }); 829 } 830 break; 831 } 832 } 833}; 834 835/** 836 * Copy all special tableDrag classes from one row's form elements to a 837 * different one, removing any special classes that the destination row 838 * may have had. 839 */ 840Drupal.tableDrag.prototype.copyDragClasses = function (sourceRow, targetRow, group) { 841 var sourceElement = $('.' + group, sourceRow); 842 var targetElement = $('.' + group, targetRow); 843 if (sourceElement.length && targetElement.length) { 844 targetElement[0].className = sourceElement[0].className; 845 } 846}; 847 848Drupal.tableDrag.prototype.checkScroll = function (cursorY) { 849 var de = document.documentElement; 850 var b = document.body; 851 852 var windowHeight = this.windowHeight = window.innerHeight || (de.clientHeight && de.clientWidth != 0 ? de.clientHeight : b.offsetHeight); 853 var scrollY = this.scrollY = (document.all ? (!de.scrollTop ? b.scrollTop : de.scrollTop) : (window.pageYOffset ? window.pageYOffset : window.scrollY)); 854 var trigger = this.scrollSettings.trigger; 855 var delta = 0; 856 857 // Return a scroll speed relative to the edge of the screen. 858 if (cursorY - scrollY > windowHeight - trigger) { 859 delta = trigger / (windowHeight + scrollY - cursorY); 860 delta = (delta > 0 && delta < trigger) ? delta : trigger; 861 return delta * this.scrollSettings.amount; 862 } 863 else if (cursorY - scrollY < trigger) { 864 delta = trigger / (cursorY - scrollY); 865 delta = (delta > 0 && delta < trigger) ? delta : trigger; 866 return -delta * this.scrollSettings.amount; 867 } 868}; 869 870Drupal.tableDrag.prototype.setScroll = function (scrollAmount) { 871 var self = this; 872 873 this.scrollInterval = setInterval(function () { 874 // Update the scroll values stored in the object. 875 self.checkScroll(self.currentMouseCoords.y); 876 var aboveTable = self.scrollY > self.table.topY; 877 var belowTable = self.scrollY + self.windowHeight < self.table.bottomY; 878 if (scrollAmount > 0 && belowTable || scrollAmount < 0 && aboveTable) { 879 window.scrollBy(0, scrollAmount); 880 } 881 }, this.scrollSettings.interval); 882}; 883 884Drupal.tableDrag.prototype.restripeTable = function () { 885 // :even and :odd are reversed because jQuery counts from 0 and 886 // we count from 1, so we're out of sync. 887 // Match immediate children of the parent element to allow nesting. 888 $('> tbody > tr.draggable:visible, > tr.draggable:visible', this.table) 889 .removeClass('odd even') 890 .filter(':odd').addClass('even').end() 891 .filter(':even').addClass('odd'); 892}; 893 894/** 895 * Stub function. Allows a custom handler when a row begins dragging. 896 */ 897Drupal.tableDrag.prototype.onDrag = function () { 898 return null; 899}; 900 901/** 902 * Stub function. Allows a custom handler when a row is dropped. 903 */ 904Drupal.tableDrag.prototype.onDrop = function () { 905 return null; 906}; 907 908/** 909 * Constructor to make a new object to manipulate a table row. 910 * 911 * @param tableRow 912 * The DOM element for the table row we will be manipulating. 913 * @param method 914 * The method in which this row is being moved. Either 'keyboard' or 'mouse'. 915 * @param indentEnabled 916 * Whether the containing table uses indentations. Used for optimizations. 917 * @param maxDepth 918 * The maximum amount of indentations this row may contain. 919 * @param addClasses 920 * Whether we want to add classes to this row to indicate child relationships. 921 */ 922Drupal.tableDrag.prototype.row = function (tableRow, method, indentEnabled, maxDepth, addClasses) { 923 this.element = tableRow; 924 this.method = method; 925 this.group = [tableRow]; 926 this.groupDepth = $('.indentation', tableRow).length; 927 this.changed = false;
928 this.table = $(tableRow).closest('table').get(0); 929 this.indentEnabled = indentEnabled; 930 this.maxDepth = maxDepth; 931 this.direction = ''; // Direction the row is being moved. 932 933 if (this.indentEnabled) { 934 this.indents = $('.indentation', tableRow).length; 935 this.children = this.findChildren(addClasses); 936 this.group = $.merge(this.group, this.children); 937 // Find the depth of this entire group. 938 for (var n = 0; n < this.group.length; n++) { 939 this.groupDepth = Math.max($('.indentation', this.group[n]).length, this.groupDepth); 940 } 941 } 942}; 943 944/** 945 * Find all children of rowObject by indentation. 946 * 947 * @param addClasses 948 * Whether we want to add classes to this row to indicate child relationships. 949 */ 950Drupal.tableDrag.prototype.row.prototype.findChildren = function (addClasses) { 951 var parentIndentation = this.indents; 952 var currentRow = $(this.element, this.table).next('tr.draggable'); 953 var rows = []; 954 var child = 0; 955 while (currentRow.length) { 956 var rowIndentation = $('.indentation', currentRow).length; 957 // A greater indentation indicates this is a child. 958 if (rowIndentation > parentIndentation) { 959 child++; 960 rows.push(currentRow[0]); 961 if (addClasses) { 962 $('.indentation', currentRow).each(function (indentNum) { 963 if (child == 1 && (indentNum == parentIndentation)) { 964 $(this).addClass('tree-child-first'); 965 } 966 if (indentNum == parentIndentation) { 967 $(this).addClass('tree-child'); 968 } 969 else if (indentNum > parentIndentation) { 970 $(this).addClass('tree-child-horizontal'); 971 } 972 }); 973 } 974 } 975 else { 976 break; 977 } 978 currentRow = currentRow.next('tr.draggable'); 979 } 980 if (addClasses && rows.length) { 981 $('.indentation:nth-child(' + (parentIndentation + 1) + ')', rows[rows.length - 1]).addClass('tree-child-last'); 982 } 983 return rows; 984}; 985 986/** 987 * Ensure that two rows are allowed to be swapped. 988 * 989 * @param row 990 * DOM object for the row being considered for swapping. 991 */ 992Drupal.tableDrag.prototype.row.prototype.isValidSwap = function (row) { 993 if (this.indentEnabled) { 994 var prevRow, nextRow; 995 if (this.direction == 'down') { 996 prevRow = row; 997 nextRow = $(row).next('tr').get(0); 998 } 999 else { 1000 prevRow = $(row).prev('tr').get(0); 1001 nextRow = row; 1002 } 1003 this.interval = this.validIndentInterval(prevRow, nextRow); 1004 1005 // We have an invalid swap if the valid indentations interval is empty. 1006 if (this.interval.min > this.interval.max) { 1007 return false; 1008 } 1009 } 1010 1011 // Do not let an un-draggable first row have anything put before it. 1012 if (this.table.tBodies[0].rows[0] == row && $(row).is(':not(.draggable)')) { 1013 return false; 1014 } 1015 1016 return true; 1017}; 1018 1019/** 1020 * Perform the swap between two rows. 1021 * 1022 * @param position 1023 * Whether the swap will occur 'before' or 'after' the given row. 1024 * @param row 1025 * DOM element what will be swapped with the row group. 1026 */ 1027Drupal.tableDrag.prototype.row.prototype.swap = function (position, row) { 1028 Drupal.detachBehaviors(this.group, Drupal.settings, 'move'); 1029 $(row)[position](this.group); 1030 Drupal.attachBehaviors(this.group, Drupal.settings); 1031 this.changed = true; 1032 this.onSwap(row); 1033}; 1034 1035/** 1036 * Determine the valid indentations interval for the row at a given position 1037 * in the table. 1038 * 1039 * @param prevRow 1040 * DOM object for the row before the tested position 1041 * (or null for first position in the table). 1042 * @param nextRow 1043 * DOM object for the row after the tested position 1044 * (or null for last position in the table). 1045 */ 1046Drupal.tableDrag.prototype.row.prototype.validIndentInterval = function (prevRow, nextRow) { 1047 var minIndent, maxIndent; 1048 1049 // Minimum indentation: 1050 // Do not orphan the next row. 1051 minIndent = nextRow ? $('.indentation', nextRow).length : 0; 1052 1053 // Maximum indentation: 1054 if (!prevRow || $(prevRow).is(':not(.draggable)') || $(this.element).is('.tabledrag-root')) { 1055 // Do not indent: 1056 // - the first row in the table, 1057 // - rows dragged below a non-draggable row, 1058 // - 'root' rows. 1059 maxIndent = 0; 1060 } 1061 else {
1062 // Do not go deeper than as a child of the previous row. 1063 maxIndent = $('.indentation', prevRow).length + ($(prevRow).is('.tabledrag-leaf') ? 0 : 1); 1064 // Limit by the maximum allowed depth for the table. 1065 if (this.maxDepth) { 1066 maxIndent = Math.min(maxIndent, this.maxDepth - (this.groupDepth - this.indents)); 1067 } 1068 } 1069 1070 return { 'min': minIndent, 'max': maxIndent }; 1071}; 1072 1073/** 1074 * Indent a row within the legal bounds of the table. 1075 * 1076 * @param indentDiff 1077 * The number of additional indentations proposed for the row (can be 1078 * positive or negative). This number will be adjusted to nearest valid 1079 * indentation level for the row. 1080 */ 1081Drupal.tableDrag.prototype.row.prototype.indent = function (indentDiff) { 1082 // Determine the valid indentations interval if not available yet. 1083 if (!this.interval) { 1084 var prevRow = $(this.element).prev('tr').get(0); 1085 var nextRow = $(this.group).filter(':last').next('tr').get(0); 1086 this.interval = this.validIndentInterval(prevRow, nextRow); 1087 } 1088 1089 // Adjust to the nearest valid indentation. 1090 var indent = this.indents + indentDiff; 1091 indent = Math.max(indent, this.interval.min); 1092 indent = Math.min(indent, this.interval.max); 1093 indentDiff = indent - this.indents; 1094 1095 for (var n = 1; n <= Math.abs(indentDiff); n++) { 1096 // Add or remove indentations. 1097 if (indentDiff < 0) { 1098 $('.indentation:first', this.group).remove(); 1099 this.indents--; 1100 } 1101 else { 1102 $('td:first', this.group).prepend(Drupal.theme('tableDragIndentation')); 1103 this.indents++; 1104 } 1105 } 1106 if (indentDiff) { 1107 // Update indentation for this row. 1108 this.changed = true; 1109 this.groupDepth += indentDiff; 1110 this.onIndent(); 1111 } 1112 1113 return indentDiff; 1114}; 1115 1116/** 1117 * Find all siblings for a row, either according to its subgroup or indentation. 1118 * Note that the passed-in row is included in the list of siblings. 1119 * 1120 * @param settings 1121 * The field settings we're using to identify what constitutes a sibling. 1122 */ 1123Drupal.tableDrag.prototype.row.prototype.findSiblings = function (rowSettings) { 1124 var siblings = []; 1125 var directions = ['prev', 'next']; 1126 var rowIndentation = this.indents; 1127 for (var d = 0; d < directions.length; d++) { 1128 var checkRow = $(this.element)[directions[d]](); 1129 while (checkRow.length) { 1130 // Check that the sibling contains a similar target field. 1131 if ($('.' + rowSettings.target, checkRow)) { 1132 // Either add immediately if this is a flat table, or check to ensure 1133 // that this row has the same level of indentation. 1134 if (this.indentEnabled) { 1135 var checkRowIndentation = $('.indentation', checkRow).length; 1136 } 1137 1138 if (!(this.indentEnabled) || (checkRowIndentation == rowIndentation)) { 1139 siblings.push(checkRow[0]); 1140 } 1141 else if (checkRowIndentation < rowIndentation) { 1142 // No need to keep looking for siblings when we get to a parent. 1143 break; 1144 } 1145 } 1146 else { 1147 break; 1148 } 1149 checkRow = $(checkRow)[directions[d]](); 1150 } 1151 // Since siblings are added in reverse order for previous, reverse the 1152 // completed list of previous siblings. Add the current row and continue. 1153 if (directions[d] == 'prev') { 1154 siblings.reverse(); 1155 siblings.push(this.element); 1156 } 1157 } 1158 return siblings; 1159}; 1160 1161/** 1162 * Remove indentation helper classes from the current row group. 1163 */ 1164Drupal.tableDrag.prototype.row.prototype.removeIndentClasses = function () { 1165 for (var n in this.children) { 1166 $('.indentation', this.children[n]) 1167 .removeClass('tree-child') 1168 .removeClass('tree-child-first') 1169 .removeClass('tree-child-last') 1170 .removeClass('tree-child-horizontal'); 1171 } 1172}; 1173 1174/** 1175 * Add an asterisk or other marker to the changed row. 1176 */ 1177Drupal.tableDrag.prototype.row.prototype.markChanged = function () { 1178 var marker = Drupal.theme('tableDragChangedMarker'); 1179 var cell = $('td:first', this.element); 1180 if ($('span.tabledrag-changed', cell).length == 0) { 1181 cell.append(marker); 1182 } 1183}; 1184 1185/** 1186 * Stub function. Allows a custom handler when a row is indented. 1187 */ 1188Drupal.tableDrag.prototype.row.prototype.onIndent = function () { 1189 return null; 1190}; 1191 1192/** 1193 * Stub function. Allows a custom handler when a row is swapped. 1194 */ 1195Drupal.tableDrag.prototype.row.prototype.onSwap = function (swappedRow) { 1196 return null; 1197}; 1198 1199Drupal.theme.prototype.tableDragChangedMarker = function () { 1200 return '<span class="warning tabledrag-changed">*</span>'; 1201}; 1202 1203Drupal.theme.prototype.tableDragIndentation = function () { 1204 return '<div class="indentation"> </div>'; 1205}; 1206 1207Drupal.theme.prototype.tableDragChangedWarning = function () { 1208 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>'; 1209}; 1210 1211})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.