Index: misc/tabledrag.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/tabledrag.js,v
retrieving revision 1.9
diff -u -r1.9 tabledrag.js
--- misc/tabledrag.js	6 Dec 2007 09:53:53 -0000	1.9
+++ misc/tabledrag.js	13 Dec 2007 22:16:52 -0000
@@ -267,8 +267,22 @@
           self.safeBlur = false; // Do not allow the onBlur cleanup.
           self.rowObject.direction = 'up';
           keyChange = true;
-          if (self.rowObject.isValidSwap(previousRow, 0)) {
+
+          if ($(item).is('.no-parent')) {
+            // Switch with the first top-level row we find up the table.
+            var groupHeight = 0;
+            while (previousRow && $('.indentation', previousRow).size()) {
+              previousRow = $(previousRow).prev('tr').get(0);
+              groupHeight += previousRow.offsetHeight;
+            }
+            if (previousRow) {
+              self.rowObject.swap('before', previousRow);
+              window.scrollBy(0, -groupHeight);
+            }
+          }
+          else if (self.rowObject.isValidSwap(previousRow, 0)) {
             self.rowObject.swap('before', previousRow);
+            self.rowObject.indent(0);
             window.scrollBy(0, -parseInt(item.offsetHeight));
           }
           else if (self.table.tBodies[0].rows[0] != previousRow || $(previousRow).is('.draggable')) {
@@ -294,15 +308,29 @@
           self.safeBlur = false; // Do not allow the onBlur cleanup.
           self.rowObject.direction = 'down';
           keyChange = true;
-          if (self.rowObject.isValidSwap(nextRow, 0)) {
+
+          if ($(item).is('.no-parent')) {
+            // Switch with last row of the next group
+            var groupHeight = 0;
+            nextGroup = new self.row(nextRow, 'keyboard', self.indentEnabled, self.maxDepth, false);
+            if (nextGroup) {
+              $(nextGroup.group).each(function () {groupHeight += this.offsetHeight});
+              nextGroupRow = $(nextGroup.group).filter(':last').get(0);
+              self.rowObject.swap('after', nextGroupRow);
+              window.scrollBy(0, parseInt(groupHeight));
+            }
+          }
+          else if (self.rowObject.isValidSwap(nextRow, 0)) {
             self.rowObject.swap('after', nextRow);
+            self.rowObject.indent(0);
+            window.scrollBy(0, parseInt(item.offsetHeight));
           }
           else {
             self.rowObject.swap('after', nextRow);
             self.rowObject.indent(-1);
+            window.scrollBy(0, parseInt(item.offsetHeight));
           }
           handle.get(0).focus(); // Regain focus after the DOM manipulation.
-          window.scrollBy(0, parseInt(item.offsetHeight));
         }
         break;
     }
@@ -375,7 +403,7 @@
     }
 
     // Similar to row swapping, handle indentations.
-    if (self.indentEnabled && x != self.oldX) {
+    if (self.indentEnabled) {
       self.oldX = x;
       var xDiff = self.currentMouseCoords.x - self.dragObject.indentMousePos.x;
       // Set the number of indentations the mouse has been moved left or right.
@@ -383,17 +411,14 @@
       // Limit the indentation to no less than the left edge of the table and no
       // more than the total amount of indentation in the table.
       indentDiff = indentDiff > 0 ? Math.min(indentDiff, self.indentCount - self.rowObject.indents + 1) : Math.max(indentDiff, -self.rowObject.indents);
-      if (indentDiff != 0) {
-        // Indent the row with our estimated diff, which may be further
-        // restricted according to the rows around this row.
-        var indentChange = self.rowObject.indent(indentDiff);
-
-        // Update table and mouse indentations.
-        self.dragObject.indentMousePos.x += self.indentAmount * indentChange;
-        self.indentCount = Math.max(self.indentCount, self.rowObject.indents);
-      }
+      // Indent the row with our estimated diff, which may be further
+      // restricted according to the rows around this row.
+      var indentChange = self.rowObject.indent(indentDiff);
+
+      // Update table and mouse indentations.
+      self.dragObject.indentMousePos.x += self.indentAmount * indentChange;
+      self.indentCount = Math.max(self.indentCount, self.rowObject.indents);
     }
-
     return false;
   }
 };
@@ -547,7 +572,7 @@
         return null;
       }
 
-      // We've may have found the row the mouse just passed over, but it doesn't
+      // We may have found the row the mouse just passed over, but it doesn't
       // take into account hidden rows. Skip backwards until we find a draggable
       // row.
       while ($(row).is(':hidden') && $(row).prev('tr').is(':hidden')) {
@@ -861,19 +886,25 @@
     var rowIndents = $('.indentation', row).size();
     var prevIndents = $('.indentation', $(row).prev('tr')).size();
     var nextIndents = $('.indentation', $(row).next('tr')).size();
+    var indentForbidden = $(this.element).is('.no-parent');
 
     if (
       (this.direction == 'down') && (
         // Prevent being able to drag a row downward with 2 indentations from a parent.
         this.indents > rowIndents + 1 ||
         // Prevent orphaning children when dragging into a parent.
-        this.indents < nextIndents - indentDiff
+        this.indents < nextIndents - indentDiff ||
+        // Prevent breaking hierarchy when dragging into a parent if the
+        // current row can't be nested.
+        (indentForbidden && nextIndents)
       ) ||
       (this.direction == 'up') && (
         // Prevent being able to drag a row upward with 2 indentations from a parent.
         this.indents < rowIndents ||
         // Prevent orphaning children when dragging between a child and parent.
         this.indents > prevIndents + 1 - indentDiff
+        // The first test also catches the "Prevent breaking hierarchy when
+        // dragging into a parent if the current row can't be nested" case.
       )
     ) {
       return false;
@@ -915,13 +946,13 @@
  */
 Drupal.tableDrag.prototype.row.prototype.indent = function(indentDiff) {
   if (indentDiff > 0) {
-    var prevRow = $(this.group).filter(':first').prev('tr').get(0);
+    var prevRow = $(this.element).prev('tr').get(0);
     if (prevRow) {
-      var prevIndent = $('.indentation', $(this.group).filter(':first').prev('tr')).size();
-      indentDiff = Math.min(prevIndent - this.indents + 1, indentDiff);
+      var parentIndent = $('.indentation', prevRow).size();
+      indentDiff = Math.min(parentIndent - this.indents + (!$(prevRow).is('.no-child')), indentDiff);
     }
     else {
-      indentDiff = 0; // First row may not contain indents.
+      indentDiff = -this.indents; // First row may not contain indents.
     }
   }
   else {
@@ -929,7 +960,7 @@
     indentDiff = Math.max(nextIndent - this.indents, indentDiff);
   }
 
-  // Never allow indentation greater the set limit.
+  // Never allow indentation greater than the set limit.
   if (this.maxDepth && indentDiff + this.groupDepth > this.maxDepth) {
     indentDiff = 0;
   }
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.734
diff -u -r1.734 common.inc
--- includes/common.inc	12 Dec 2007 14:54:27 -0000	1.734
+++ includes/common.inc	13 Dec 2007 22:42:21 -0000
@@ -2076,6 +2076,12 @@
  * );
  * @endcode
  *
+ * Additionnally, when tree relationships are present, the two additional
+ * 'no-child' and 'no-parent' classes can be used to refine the behavior:
+ * - Rows with the 'no-child' class are leaves that cannot have child rows.
+ * - Rows with the 'no-parent' class are roots that cannot be nested under
+ *   a parent row.
+ *
  * Calling drupal_add_tabledrag() would then be written as such:
  * @code
  * drupal_add_tabledrag('my-module-table', 'order', 'sibling', 'my-elements-weight');
