Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.855
diff -u -p -r1.855 common.inc
--- includes/common.inc	22 Jan 2009 05:01:39 -0000	1.855
+++ includes/common.inc	23 Jan 2009 07:17:32 -0000
@@ -3292,7 +3292,10 @@ function drupal_render(&$elements) {
     }
     $prefix = isset($elements['#prefix']) ? $elements['#prefix'] : '';
     $suffix = isset($elements['#suffix']) ? $elements['#suffix'] : '';
-    return $prefix . $content . $suffix;
+    $content = $prefix . $content . $suffix;
+    // Store the rendered content, so higher level elements can reuse it.
+    $elements['#content'] = $content;
+    return $content;
   }
 }
 
@@ -3544,6 +3547,9 @@ function drupal_common_theme() {
     'file' => array(
       'arguments' => array('element' => NULL),
     ),
+    'tableselect' => array(
+      'arguments' => array('element' => NULL),
+    ),
     'form_element' => array(
       'arguments' => array('element' => NULL, 'value' => NULL),
     ),
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.316
diff -u -p -r1.316 form.inc
--- includes/form.inc	22 Jan 2009 12:46:05 -0000	1.316
+++ includes/form.inc	23 Jan 2009 07:20:08 -0000
@@ -291,7 +291,7 @@ function form_set_cache($form_build_id, 
  */
 function drupal_execute($form_id, &$form_state) {
   $args = func_get_args();
-  
+
   // Make sure $form_state is passed around by reference.
   $args[1] = &$form_state;
 
@@ -2037,6 +2037,117 @@ function form_process_checkboxes($elemen
 }
 
 /**
+ * Format a table with radio buttons or checkboxes.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   tableselect element.
+ *   Properties used: header, options, empty, js_select.
+ *
+ * @return
+ *   A themed HTML string representing the table.
+ *
+ * @ingroup themeable
+ */
+function theme_tableselect($element) {
+  $rows = array();
+  if (!empty($element['#options'])) {
+    // Generate a table row for each selectable item in #options.
+    foreach ($element['#options'] as $key => $value) {
+      $row = array();
+
+      // Render the checkbox / radio element.
+      $row[] = $element[$key]['#content'];
+
+      // As theme_table only maps header and row columns by order, create the
+      // correct order by iterating over the header fields.
+      foreach ($element['#header'] as $fieldname => $title) {
+        $row[] = $element['#options'][$key][$fieldname];
+      }
+      $rows[] = $row;
+    }
+    // Add an empty header or a "Select all" checkbox to provide room for the
+    // checkboxes/radios in the first table column.
+    $first_col = $element['#js_select'] ? array(theme('table_select_header_cell')) : array('');
+    $header = array_merge($first_col, $element['#header']);
+  }
+  else {
+    // If there are no selectable options, display the empty text over the
+    // entire width of the table.
+    $header = $element['#header'];
+    $rows[] = array(array('data' => $element['#empty'], 'colspan' => count($header)));
+  }
+  return theme('table', $header, $rows);
+}
+
+/**
+ * Create the correct amount of checkbox or radio elements to populate the table.
+ *
+ * @param $element
+ *   An associative array containing the properties and children of the
+ *   tableselect element.
+ *
+ * @return
+ *   The processed element.
+ */
+function form_process_tableselect($element) {
+
+  if ($element['#multiple']) {
+    $value = is_array($element['#value']) ? $element['#value'] : array();
+  }
+  else {
+    // Advanced selection behaviour make no sense for radios.
+    $element['#js_select'] = FALSE;
+  }
+
+  $element['#tree'] = TRUE;
+
+  if (count($element['#options']) > 0) {
+    if (!isset($element['#default_value']) || $element['#default_value'] === 0) {
+      $element['#default_value'] = array();
+    }
+
+    // Create a checkbox or radio for each item in #options in such a way that
+    // the value of the tableselect element behaves as if it had been of type
+    // checkboxes or radios.
+    foreach ($element['#options'] as $key => $choice) {
+      // Do not overwrite manually created children.
+      if (!isset($element[$key])) {
+        if ($element['#multiple']) {
+          $element[$key] = array(
+            '#type' => 'checkbox',
+            '#title' => '',
+            '#return_value' => $key,
+            '#default_value' => isset($value[$key]),
+            '#attributes' => $element['#attributes'],
+            '#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL,
+          );
+        }
+        else {
+          // Generate the parents as the autogenerator does, so we will have a
+          // unique id for each radio button.
+          $parents_for_id = array_merge($element['#parents'], array($key));
+          $element[$key] = array(
+            '#type' => 'radio',
+            '#title' => '',
+            '#return_value' => $key,
+            '#default_value' => ($element['#default_value'] == $key) ? $key : NULL,
+            '#attributes' => $element['#attributes'],
+            '#parents' => $element['#parents'],
+            '#id' => form_clean_id('edit-' . implode('-', $parents_for_id)),
+            '#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL,
+          );
+        }
+      }
+    }
+  }
+  else {
+    $element['#value'] = array();
+  }
+  return $element;
+}
+
+/**
  * Theme a form submit button.
  *
  * @ingroup themeable
Index: includes/tablesort.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/tablesort.inc,v
retrieving revision 1.48
diff -u -p -r1.48 tablesort.inc
--- includes/tablesort.inc	14 Apr 2008 17:48:33 -0000	1.48
+++ includes/tablesort.inc	23 Jan 2009 07:17:32 -0000
@@ -165,13 +165,14 @@ function tablesort_get_order($headers) {
     return $default;
   }
   else {
-    // The first column specified is initial 'order by' field unless otherwise specified
-    if (is_array($headers[0])) {
-      $headers[0] += array('data' => NULL, 'field' => NULL);
-      return array('name' => $headers[0]['data'], 'sql' => $headers[0]['field']);
+    // The first column specified is the initial 'order by' field unless otherwise specified.
+    $first = current($headers);
+    if (is_array($first)) {
+      $first += array('data' => NULL, 'field' => NULL);
+      return array('name' => $first['data'], 'sql' => $first['field']);
     }
     else {
-      return array('name' => $headers[0]);
+      return array('name' => $first, 'sql' => '');
     }
   }
 }
Index: modules/comment/comment.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.admin.inc,v
retrieving revision 1.12
diff -u -p -r1.12 comment.admin.inc
--- modules/comment/comment.admin.inc	3 Dec 2008 16:32:21 -0000	1.12
+++ modules/comment/comment.admin.inc	23 Jan 2009 07:17:32 -0000
@@ -58,43 +58,37 @@ function comment_admin_overview($type = 
 
   // Load the comments that need to be displayed.
   $status = ($arg == 'approval') ? COMMENT_NOT_PUBLISHED : COMMENT_PUBLISHED;
-  $form['header'] = array(
-    '#type' => 'value',
-    '#value' => array(
-      theme('table_select_header_cell'),
-      array('data' => t('Subject'), 'field' => 'subject'),
-      array('data' => t('Author'), 'field' => 'name'),
-      array('data' => t('Posted in'), 'field' => 'node_title'),
-      array('data' => t('Time'), 'field' => 'timestamp', 'sort' => 'desc'),
-      array('data' => t('Operations')),
-  ));
-  $result = pager_query('SELECT c.subject, c.nid, c.cid, c.comment, c.timestamp, c.status, c.name, c.homepage, u.name AS registered_name, u.uid, n.title as node_title FROM {comment} c INNER JOIN {users} u ON u.uid = c.uid INNER JOIN {node} n ON n.nid = c.nid WHERE c.status = %d' . tablesort_sql($form['header']['#value']), 50, 0, NULL, $status);
+  $header = array(
+    'subject' => array('data' => t('Subject'), 'field' => 'subject'),
+    'author' => array('data' => t('Author'), 'field' => 'name'),
+    'posted_in' => array('data' => t('Posted in'), 'field' => 'node_title'),
+    'time' => array('data' => t('Time'), 'field' => 'timestamp', 'sort' => 'desc'),
+    'operations' => array('data' => t('Operations')),
+  );
+
+  $result = pager_query('SELECT c.subject, c.nid, c.cid, c.comment, c.timestamp, c.status, c.name, c.homepage, u.name AS registered_name, u.uid, n.title as node_title FROM {comment} c INNER JOIN {users} u ON u.uid = c.uid INNER JOIN {node} n ON n.nid = c.nid WHERE c.status = %d' . tablesort_sql($header), 50, 0, NULL, $status);
 
   // Build a table listing the appropriate comments.
+  $options = array();
   $destination = drupal_get_destination();
+
   while ($comment = db_fetch_object($result)) {
-    $comments[$comment->cid] = '';
-    $comment->name = $comment->uid ? $comment->registered_name : $comment->name;
-    $form['subject'][$comment->cid] = array(
-      '#markup' => l($comment->subject, 'node/' . $comment->nid, array('attributes' => array('title' => truncate_utf8($comment->comment, 128)), 'fragment' => 'comment-' . $comment->cid))
-    );
-    $form['username'][$comment->cid] = array(
-      '#markup' => theme('username', $comment)
-    );
-    $form['node_title'][$comment->cid] = array(
-      '#markup' => l($comment->node_title, 'node/' . $comment->nid)
-    );
-    $form['timestamp'][$comment->cid] = array(
-      '#markup' => format_date($comment->timestamp, 'small')
-    );
-    $form['operations'][$comment->cid] = array(
-      '#markup' => l(t('edit'), 'comment/edit/' . $comment->cid, array('query' => $destination))
+    $options[$comment->cid] = array(
+      'subject' => l($comment->subject, 'node/' . $comment->nid, array('attributes' => array('title' => truncate_utf8($comment->comment, 128)), 'fragment' => 'comment-' . $comment->cid)),
+      'author' => theme('username', $comment),
+      'posted_in' => l($comment->node_title, 'node/' . $comment->nid),
+      'time' => format_date($comment->timestamp, 'small'),
+      'operations' => l(t('edit'), 'comment/edit/' . $comment->cid, array('query' => $destination)),
     );
   }
+
   $form['comments'] = array(
-    '#type' => 'checkboxes',
-    '#options' => isset($comments) ? $comments: array()
+    '#type' => 'tableselect',
+    '#header' => $header,
+    '#options' => $options,
+    '#empty' => t('No comments available.'),
   );
+
   $form['pager'] = array(
     '#markup' => theme('pager', NULL, 50, 0)
   );
@@ -146,41 +140,6 @@ function comment_admin_overview_submit($
 }
 
 /**
- * Theme the comment admin form.
- *
- * @param $form
- *   An associative array containing the structure of the form.
- * @ingroup themeable
- */
-function theme_comment_admin_overview($form) {
-  $output = drupal_render($form['options']);
-  if (isset($form['subject']) && is_array($form['subject'])) {
-    foreach (element_children($form['subject']) as $key) {
-      $row = array();
-      $row[] = drupal_render($form['comments'][$key]);
-      $row[] = drupal_render($form['subject'][$key]);
-      $row[] = drupal_render($form['username'][$key]);
-      $row[] = drupal_render($form['node_title'][$key]);
-      $row[] = drupal_render($form['timestamp'][$key]);
-      $row[] = drupal_render($form['operations'][$key]);
-      $rows[] = $row;
-    }
-  }
-  else {
-    $rows[] = array(array('data' => t('No comments available.'), 'colspan' => '6'));
-  }
-
-  $output .= theme('table', $form['header']['#value'], $rows);
-  if ($form['pager']['#markup']) {
-    $output .= drupal_render($form['pager']);
-  }
-
-  $output .= drupal_render($form);
-
-  return $output;
-}
-
-/**
  * List the selected comments and verify that the admin wants to delete them.
  *
  * @param $form_state
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.685
diff -u -p -r1.685 comment.module
--- modules/comment/comment.module	22 Jan 2009 12:46:06 -0000	1.685
+++ modules/comment/comment.module	23 Jan 2009 07:17:32 -0000
@@ -118,9 +118,6 @@ function comment_theme() {
     'comment_block' => array(
       'arguments' => array(),
     ),
-    'comment_admin_overview' => array(
-      'arguments' => array('form' => NULL),
-    ),
     'comment_preview' => array(
       'arguments' => array('comment' => NULL, 'node' => NULL, 'links' => array(), 'visible' => 1),
     ),
Index: modules/simpletest/tests/form.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/form.test,v
retrieving revision 1.2
diff -u -p -r1.2 form.test
--- modules/simpletest/tests/form.test	17 Jan 2009 19:27:32 -0000	1.2
+++ modules/simpletest/tests/form.test	26 Jan 2009 10:18:42 -0000
@@ -110,3 +110,206 @@ class FormsTestTypeCase extends DrupalWe
   }
 }
 
+/**
+ * Test the tableselect form element for expected behavior.
+ */
+class FormsElementsTableSelectFunctionalTest extends DrupalWebTestCase {
+
+  function getInfo() {
+    return array(
+      'name' => t('Tableselect form element type test'),
+      'description' => t('Test the tableselect element for expected behavior'),
+      'group' => t('Form API'),
+    );
+  }
+
+  function setUp() {
+    parent::setUp('form_test');
+  }
+
+
+  /**
+   * Test the display of checkboxes when #multiple is TRUE.
+   */
+  function testMultipleTrue() {
+
+    $this->drupalGet('form_test/tableselect/multiple-true');
+
+    $this->assertNoText(t('Empty text.'), t('Empty text should not be displayed.'));
+
+    // Test for the presence of the Select all rows tableheader.
+    $this->assertFieldByXPath('//th[@class="select-all"]', NULL, t('Presence of the "Select all" checkbox.'));
+
+    $rows = array('row1', 'row2', 'row3');
+    foreach($rows as $row) {
+      $this->assertFieldByXPath('//input[@type="checkbox"]', $row, t('Checkbox for value @row.', array('@row' => $row)));
+    }
+  }
+
+  /**
+   * Test the display of radios when #multiple is FALSE.
+   */
+  function testMultipleFalse() {
+    $this->drupalGet('form_test/tableselect/multiple-false');
+
+    $this->assertNoText(t('Empty text.'), t('Empty text should not be displayed.'));
+
+    // Test for the absence of the Select all rows tableheader.
+    $this->assertNoFieldByXPath('//th[@class="select-all"]', '', t('Absence of the "Select all" checkbox.'));
+
+    $rows = array('row1', 'row2', 'row3');
+    foreach($rows as $row) {
+      $this->assertFieldByXPath('//input[@type="radio"]', $row, t('Radio button for value @row.', array('@row' => $row)));
+    }
+  }
+
+  /**
+   * Test the display of the #empty text when #options is an empty array.
+   */
+  function testEmptyText() {
+    $this->drupalGet('form_test/tableselect/empty-text');
+    $this->assertText(t('Empty text.'), t('Empty text should be displayed.'));
+  }
+
+  /**
+   * Test the submission of single and multiple values when #multiple is TRUE.
+   */
+  function testMultipleTrueSubmit() {
+
+    // Test a submission with one checkbox checked.
+    $edit = array();
+    $edit['tableselect[row1]'] = TRUE;
+    $this->drupalPost('form_test/tableselect/multiple-true', $edit, 'Submit');
+
+    $this->assertText(t('Submitted: row1 = row1'), t('Checked checkbox row1'));
+    $this->assertText(t('Submitted: row2 = 0'), t('Unchecked checkbox row2.'));
+    $this->assertText(t('Submitted: row3 = 0'), t('Unchecked checkbox row3.'));
+
+    // Test a submission with multiple checkboxes checked.
+    $edit['tableselect[row1]'] = TRUE;
+    $edit['tableselect[row3]'] = TRUE;
+    $this->drupalPost('form_test/tableselect/multiple-true', $edit, 'Submit');
+
+    $this->assertText(t('Submitted: row1 = row1'), t('Checked checkbox row1.'));
+    $this->assertText(t('Submitted: row2 = 0'), t('Unchecked checkbox row2.'));
+    $this->assertText(t('Submitted: row3 = row3'), t('Checked checkbox row3.'));
+
+  }
+
+  /**
+   * Test submission of values when #multiple is FALSE.
+   */
+  function testMultipleFalseSubmit() {
+    $edit['tableselect'] = 'row1';
+    $this->drupalPost('form_test/tableselect/multiple-false', $edit, 'Submit');
+    $this->assertText(t('Submitted: row1'), t('Selected radio button'));
+  }
+
+  /**
+   * Test the #js_select property.
+   */
+  function testAdvancedSelect() {
+    // When #multiple = TRUE a Select all checkbox should be displayed by default.
+    $this->drupalGet('form_test/tableselect/advanced-select/multiple-true-default');
+    $this->assertFieldByXPath('//th[@class="select-all"]', NULL, t('Display a "Select all" checkbox by default when #multiple is TRUE.'));
+
+    // When #js_select is set to FALSE, a "Select all" checkbox should not be displayed.
+    $this->drupalGet('form_test/tableselect/advanced-select/multiple-true-no-advanced-select');
+    $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, t('Do not display a "Select all" checkbox when #js_select is FALSE.'));
+
+    // A "Select all" checkbox never makes sense when #multiple = FALSE, regardless of the value of #js_select.
+    $this->drupalGet('form_test/tableselect/advanced-select/multiple-false-default');
+    $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, t('Do not display a "Select all" checkbox when #multiple is FALSE.'));
+
+    $this->drupalGet('form_test/tableselect/advanced-select/multiple-false-advanced-select');
+    $this->assertNoFieldByXPath('//th[@class="select-all"]', NULL, t('Do not display a "Select all" checkbox when #multiple is FALSE, even when #js_select is TRUE.'));
+  }
+
+
+  /**
+   * Test the whether the option checker gives an error on invalid tableselect values for checkboxes.
+   */
+  function testMultipleTrueOptionchecker() {
+
+    list($header, $options) = _form_test_tableselect_get_data();
+
+    $form['tableselect'] = array(
+      '#type' => 'tableselect',
+      '#header' => $header,
+      '#options' => $options,
+    );
+
+    // Test with a valid value.
+    list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => 'row1'));
+    $this->assertFalse(isset($errors['tableselect']), t('Option checker allows valid values for checkboxes.'));
+
+    // Test with an invalid value.
+    list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => 'non_existing_value'));
+    $this->assertTrue(isset($errors['tableselect']), t('Option checker disallows invalid values for checkboxes.'));
+
+  }
+
+
+  /**
+   * Test the whether the option checker gives an error on invalid tableselect values for radios.
+   */
+  function testMultipleFalseOptionchecker() {
+
+    list($header, $options) = _form_test_tableselect_get_data();
+
+    $form['tableselect'] = array(
+      '#type' => 'tableselect',
+      '#header' => $header,
+      '#options' => $options,
+      '#multiple' => FALSE,
+    );
+
+    // Test with a valid value.
+    list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => 'row1'));
+    $this->assertFalse(isset($errors['tableselect']), t('Option checker allows valid values for radio buttons.'));
+
+    // Test with an invalid value.
+    list($processed_form, $form_state, $errors) = $this->formSubmitHelper($form, array('tableselect' => 'non_existing_value'));
+    $this->assertTrue(isset($errors['tableselect']), t('Option checker disallows invalid values for radio buttons.'));
+  }
+
+
+  /**
+   * Helper function for the option check test to submit a form while collecting errors.
+   *
+   * @param $form_element
+   *   A form element to test.
+   * @param $edit
+   *   An array containing post data.
+   *
+   * @return
+   *   An array containing the processed form, the form_state and any errors.
+   */
+  private function formSubmitHelper($form_element, $edit) {
+    $form_id = $this->randomName();
+
+    $form = $form_state = array();
+
+    $form = array_merge($form, $form_element);
+    $form['op'] = array('#type' => 'submit', '#value' => t('Submit'));
+
+    $form['#post'] = $edit;
+    $form['#post']['form_id'] = $form_id;
+
+    drupal_prepare_form($form_id, $form, $form_state);
+
+    drupal_process_form($form_id, $form, $form_state);
+
+    $errors = form_get_errors();
+
+    // Clear errors and messages.
+    drupal_get_messages();
+    form_set_error(NULL, '', TRUE);
+
+    // Return the processed form together with form_state and errors
+    // to allow the caller lowlevel access to the form.
+    return array($form, $form_state, $errors);
+  }
+
+}
+
Index: modules/simpletest/tests/form_test.info
===================================================================
RCS file: modules/simpletest/tests/form_test.info
diff -N modules/simpletest/tests/form_test.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/form_test.info	23 Jan 2009 07:17:32 -0000
@@ -0,0 +1,8 @@
+; $Id$
+name = "FormAPI Test"
+description = "Support module for Form API tests."
+package = Testing
+version = VERSION
+core = 7.x
+files[] = form_test.module
+hidden = TRUE
\ No newline at end of file
Index: modules/simpletest/tests/form_test.module
===================================================================
RCS file: modules/simpletest/tests/form_test.module
diff -N modules/simpletest/tests/form_test.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/form_test.module	26 Jan 2009 10:15:06 -0000
@@ -0,0 +1,182 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Helper module for the form API tests.
+ */
+
+/**
+ * Implementation of hook_menu().
+ */
+function form_test_menu() {
+  $items = array();
+
+  $items['form_test/tableselect/multiple-true'] = array(
+    'title' => 'Tableselect checkboxes test',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('_form_test_tableselect_multiple_true_form'),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
+
+  $items['form_test/tableselect/multiple-false'] = array(
+    'title' => 'Tableselect radio button test',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('_form_test_tableselect_multiple_false_form'),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
+
+  $items['form_test/tableselect/empty-text'] = array(
+    'title' => 'Tableselect empty text test',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('_form_test_tableselect_empty_form'),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
+
+  $items['form_test/tableselect/advanced-select'] = array(
+    'title' => 'Tableselect js_select tests',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('_form_test_tableselect_js_select_form'),
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
+/**
+ * Create a header and options array. Helper function for callbacks.
+ */
+function _form_test_tableselect_get_data() {
+  $header = array(
+    'one' => t('One'),
+    'two' => t('Two'),
+    'three' => t('Three'),
+    'four' => t('Four'),
+  );
+
+  $options['row1'] = array(
+    'one' => 'row1col1',
+    'two' => t('row1col2'),
+    'three' => t('row1col3'),
+    'four' => t('row1col4'),
+  );
+
+  $options['row2'] = array(
+    'one' => 'row2col1',
+    'two' => t('row2col2'),
+    'three' => t('row2col3'),
+    'four' => t('row2col4'),
+  );
+
+  $options['row3'] = array(
+    'one' => 'row3col1',
+    'two' => t('row3col2'),
+    'three' => t('row3col3'),
+    'four' => t('row3col4'),
+  );
+
+  return array($header, $options);
+}
+
+/**
+ * Build a form to test the tableselect element.
+ *
+ * @param $form_state
+ *   The form_state
+ * @param $element_properties
+ *   An array of element properties for the tableselect element.
+ *
+ * @return
+ *   A form with a tableselect element and a submit button.
+ */
+function _form_test_tableselect_form_builder($form_state, $element_properties) {
+  $form = array();
+
+  list($header, $options) = _form_test_tableselect_get_data();
+
+  $form['tableselect'] = $element_properties;
+
+  $form['tableselect'] += array(
+    '#type' => 'tableselect',
+    '#header' => $header,
+    '#options' => $options,
+    '#multiple' => FALSE,
+    '#empty' => t('Empty text.'),
+  );
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Submit'),
+  );
+
+  return $form;
+}
+
+/**
+ * Test the tableselect #multiple = TRUE functionality.
+ */
+function _form_test_tableselect_multiple_true_form($form_state) {
+  return _form_test_tableselect_form_builder($form_state, array('#multiple' => TRUE));
+}
+
+/**
+ * Process the tableselect #multiple = TRUE submitted values.
+ */
+function _form_test_tableselect_multiple_true_form_submit($form, &$form_state) {
+  $selected = $form_state['values']['tableselect'];
+  foreach ($selected as $key => $value) {
+    drupal_set_message(t('Submitted: @key = @value', array('@key' => $key, '@value' => $value)));
+  }
+}
+
+/**
+ * Test the tableselect #multiple = FALSE functionality.
+ */
+function _form_test_tableselect_multiple_false_form($form_state) {
+  return _form_test_tableselect_form_builder($form_state, array('#multiple' => FALSE));
+}
+
+/**
+ * Process the tableselect #multiple = FALSE submitted values.
+ */
+function _form_test_tableselect_multiple_false_form_submit($form, &$form_state) {
+  drupal_set_message(t('Submitted: @value', array('@value' => $form_state['values']['tableselect'])));
+}
+
+/**
+ * Test functionality of the tableselect #empty property.
+ */
+function _form_test_tableselect_empty_form($form_state) {
+  return _form_test_tableselect_form_builder($form_state, array('#options' => array()));
+}
+
+/**
+ * Test functionality of the tableselect #js_select property.
+ */
+function _form_test_tableselect_js_select_form($form_state, $action) {
+
+  switch ($action) {
+
+    case 'multiple-true-default':
+      $options = array('#multiple' => TRUE);
+      break;
+
+    case 'multiple-false-default':
+      $options = array('#multiple' => FALSE);
+      break;
+
+    case 'multiple-true-no-advanced-select':
+      $options = array('#multiple' => TRUE, '#js_select' => FALSE);
+      break;
+
+    case 'multiple-false-advanced-select':
+      $options = array('#multiple' => FALSE, '#js_select' => TRUE);
+      break;
+  }
+
+  return _form_test_tableselect_form_builder($form_state, $options);
+}
\ No newline at end of file
Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.660
diff -u -p -r1.660 system.module
--- modules/system/system.module	21 Jan 2009 14:22:32 -0000	1.660
+++ modules/system/system.module	23 Jan 2009 07:17:32 -0000
@@ -342,6 +342,14 @@ function system_elements() {
     '#size' => 60,
   );
 
+  $type['tableselect'] = array(
+    '#input' => TRUE,
+    '#js_select' => TRUE,
+    '#multiple' => TRUE,
+    '#process' => array('form_process_tableselect'),
+    '#options' => array(),
+    '#empty' => '',
+  );
 
   /**
    * Form structure.
