[ Index ]

PHP Cross Reference of Drupal 6 (gatewave)

title

Body

[close]

/misc/ -> tabledrag.js (source)

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


Generated: Thu Mar 24 11:18:33 2011 Cross-referenced by PHPXref 0.7