Index: modules/system/system.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.module,v
retrieving revision 1.918
diff -u -r1.918 system.module
--- modules/system/system.module	7 Apr 2010 16:35:03 -0000	1.918
+++ modules/system/system.module	8 Apr 2010 07:49:35 -0000
@@ -432,6 +432,12 @@
     '#empty' => '',
     '#theme' => 'tableselect',
   );
+  $types['options'] = array(
+    '#input' => TRUE,
+    '#multiple' => TRUE,
+    '#process' => array('form_process_options'),
+    '#theme' => 'options',
+  );
 
   // Form structure.
   $types['item'] = array(
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.449
diff -u -r1.449 form.inc
--- includes/form.inc	7 Apr 2010 04:39:59 -0000	1.449
+++ includes/form.inc	8 Apr 2010 07:49:35 -0000
@@ -2740,6 +2740,425 @@
 }
 
 /**
+ * Theme an options form element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #attributes, #key_type_toggled, #optgroups, #multiple.
+ *
+ * @return
+ *   A themed HTML string representing the form element.
+ *
+ * @ingroup themeable
+ */
+function theme_options($variables) {
+  $element = $variables['element'];
+
+  $classes = array();
+  if (isset($element['#attributes']['class'])) {
+    $classes = $element['#attributes']['class'];
+  }
+
+  $classes[] = 'form-options';
+  $classes[] = 'options-key-type-'. $element['#key_type'];
+
+  if ($element['#key_type_toggled']) {
+    $classes[] = 'options-key-custom';
+  }
+
+  if (isset($element['#optgroups']) && $element['#optgroups']) {
+    $classes[] = 'options-optgroups';
+  }
+
+  if (isset($element['#multiple']) && $element['#multiple']) {
+    $classes[] = 'options-multiple';
+  }
+
+  $options = '';
+  $options .= drupal_render($element['options_field']);
+  if (isset($element['default_value_field'])) {
+    $options .= drupal_render($element['default_value_field']);
+  }
+
+  $settings = '';
+  if (isset($element['custom_keys'])) {
+    $settings .= drupal_render($element['custom_keys']);
+  }
+  if (isset($element['multiple'])) {
+    $settings .= drupal_render($element['multiple']);
+  }
+  if (isset($element['option_settings'])) {
+    $settings .= drupal_render($element['option_settings']);
+  }
+
+  $output = '';
+  $output .= '<div class="' . implode(' ', $classes) .'">';
+  $output .= theme('fieldset', array('element' => array(
+    '#title' => t('Options'),
+    '#collapsible' => FALSE,
+    '#children' => $options,
+    '#attributes' => array('class' => array('options')),
+  )));
+
+  if (!empty($settings)) {
+    $output .= theme('fieldset', array('element' => array(
+      '#title' => t('Option settings'),
+      '#collapsible' => FALSE,
+      '#children' => $settings,
+      '#attributes' => array('class' => array('option-settings')),
+    )));
+  }
+  $output .= '</div>';
+
+  return $output;
+}
+
+/**
+ * Process a options element.
+ */
+function form_process_options($element) {
+  $element['#options'] = isset($element['#options']) ? $element['#options'] : array();
+  $element['#multiple'] = isset($element['#multiple']) ? $element['#multiple'] : FALSE;
+
+  $element['#tree'] = TRUE;
+  $element['#theme'] = 'options';
+
+  $element['#attached']['js'] = array(
+    'misc/tabledrag.js' => array('weight' => JS_LIBRARY + 5),
+    'misc/options_element.js',
+  );
+  // @todo where to put the css file?
+  $element['#attached']['css'] = array(
+    'misc/options_element.css',
+  );
+
+  // Add the key type toggle checkbox.
+  if (!isset($element['custom_keys']) && $element['#key_type'] != 'custom' && !empty($element['#key_type_toggle'])) {
+    $element['custom_keys'] = array(
+      '#title' => is_string($element['#key_type_toggle']) ? $element['#key_type_toggle'] : t('Customize keys'),
+      '#type' => 'checkbox',
+      '#default_value' => $element['#key_type_toggled'],
+      '#attributes' => array('class' => array('key-type-toggle')),
+      '#description' => t('Customizing the keys will allow you to save one value internally while showing a different option to the user.'),
+    );
+  }
+
+  // Add the multiple value toggle checkbox.
+  if (!isset($element['multiple']) && !empty($element['#multiple_toggle'])) {
+    $element['multiple'] = array(
+      '#title' => is_string($element['#multiple_toggle']) ? $element['#multiple_toggle'] : t('Allow multiple values'),
+      '#type' => 'checkbox',
+      '#default_value' => !empty($element['#multiple']),
+      '#attributes' => array('class' => array('multiple-toggle')),
+      '#description' => t('Multiple values will let users select multiple items in this list.'),
+    );
+  }
+
+  // Add the main textarea for adding options.
+  if (!isset($element['options'])) {
+    $element['options_field'] = array(
+      '#type' => 'textarea',
+      '#resizable' => TRUE,
+      '#cols' => 60,
+      '#rows' => 5,
+      '#required' => isset($element['#required']) ? $element['#required'] : FALSE,
+      '#description' => t('List options one option per line.'),
+      '#attributes' => $element['#disabled'] ? array('readonly' => 'readonly') : array(),
+    );
+
+    // If validation fails, reload the user's text even if it's not valid.
+    if (isset($element['#value']['text'])) {
+      $element['options_field']['#value'] = $element['#value']['text'];
+    }
+    // Most of the time, we'll be converting the options array into the text.
+    else {
+      $element['options_field']['#value'] = isset($element['#options']) ? form_options_to_text($element['#options'], $element['#key_type']) : '';
+    }
+
+
+    if ($element['#key_type'] == 'mixed' || $element['#key_type'] == 'numeric' || $element['#key_type'] == 'custom') {
+      $element['options_field']['#description'] .= ' ' . t('Key-value pairs may be specified by separating each option with pipes, such as <em>key|value</em>.');
+    }
+    elseif ($element['#key_type_toggle']) {
+      $element['options_field']['#description'] .= ' ' . t('If the %toggle field is checked, key-value pairs may be specified by separating each option with pipes, such as <em>key|value</em>.', array('%toggle' => $element['custom_keys']['#title']));
+    }
+    if ($element['#key_type'] == 'numeric') {
+      $element['options_field']['#description'] .= ' ' . t('This field requires all specified keys to be integers.');
+    }
+  }
+
+  // Add the field for storing default values.
+  if ($element['#default_value'] !== FALSE && !isset($element['default_value_field'])) {
+    $element['default_value_field'] = array(
+      '#title' => t('Default value'),
+      '#type' => 'textfield',
+      '#size' => 60,
+      '#value' => isset($element['#default_value']) ? ($element['#multiple'] ? implode(', ', (array) $element['#default_value']) : $element['#default_value']) : '',
+      '#description' => t('Specify the keys that should be selected by default.'),
+    );
+    if ($element['#multiple']) {
+      $element['default_value_field']['#description'] .= ' ' . t('Multiple default values may be specified by separating keys with commas.');
+    }
+  }
+
+  // Remove properties that will confuse the FAPI.
+  unset($element['#options']);
+  $element['#required'] = FALSE;
+
+  return $element;
+}
+
+/**
+ * Validate password_confirm element.
+ */
+function options_validate($element, &$element_state) {
+ // Convert text to an array of options.
+  $duplicates = array();
+  $options = form_options_from_text($element['#value']['options_text'], $element['#key_type'], empty($element['#optgroups']), $duplicates);
+
+  // Check if a key is used multiple times.
+  if (count($duplicates) == 1) {
+    form_error($element, t('The key %key has been used multiple times. Each key must be unique to display properly.', array('%key' => reset($duplicates))));
+  }
+  elseif (!empty($duplicates)) {
+    array_walk($duplicates, 'check_plain');
+    $duplicate_list = theme('item_list', array('items' => $duplicates));
+    form_error($element, t('The following keys have been used multiple times. Each key must be unique to display properly.') . $duplicate_list);
+  }
+
+  // Add the list of duplicates to the page so that we can highlight the fields.
+  if (!empty($duplicates)) {
+    drupal_add_js(array('optionsElement' => array('errors' => drupal_map_assoc($duplicates))), 'setting');
+  }
+
+  // Check if no options are specified.
+  if (empty($options) && $element['#required']) {
+    form_error($element, t('At least one option must be specified.'));
+  }
+
+  // Check for numeric keys if needed.
+  if ($element['#key_type'] == 'numeric') {
+    foreach ($options as $key => $value) {
+      if (!is_int($key)) {
+        form_error($element, t('The keys for the %title field must be integers.', array('%title' => $element['#title'])));
+        break;
+      }
+    }
+  }
+
+  // Check that the limit of options has not been exceeded.
+  if (!empty($element['#limit'])) {
+    $count = 0;
+    foreach ($options as $value) {
+      if (is_array($value)) {
+        $count += count($value);
+      }
+      else {
+        $count++;
+      }
+    }
+    if ($count > $element['#limit']) {
+      form_error($element, t('The %title field supports a maximum of @count options. Please reduce the number of options.', array('%title' => $element['#title'], '@count' => $element['#limit'])));
+    }
+  }
+}
+
+/**
+ * This function adjusts the value of the element from a text value to an array.
+ */
+function form_type_options_value(&$element, $edit = FALSE) {
+  if ($edit === FALSE) {
+     return array(
+       'options' => isset($element['#options']) ? $element['#options'] : array(),
+       'default_value' => isset($element['#default_value']) ? $element['#default_value'] : '',
+     );
+  }
+  else {
+    // Convert text to an array of options.
+    $duplicates = array();
+    $options = form_options_from_text($edit['options_field'], $element['#key_type'], empty($element['#optgroups']), $duplicates);
+
+    // Convert default value.
+    if (isset($edit['default_value_field'])) {
+      if ($element['#multiple']) {
+        $default_value = array();
+        $default_items = explode(',', $edit['default_value_field']);
+        foreach ($default_items as $key) {
+          $key = trim($key);
+          if ($value = _form_options_search($key, $options)) {
+            $default_value[] = $value;
+          }
+        }
+      }
+      else {
+        $default_value = trim($edit['default_value_field']);
+      }
+    }
+
+    return array(
+      'options' => $options,
+      'default_value' => $default_value,
+      'options_text' => $edit['options_field'],
+      'default_value_text' => $edit['default_value_field'],
+    );
+  }
+}
+
+/**
+ * Create a textual representation of options from an array.
+ *
+ * @param $options
+ *   An array of options used in a select list.
+ * @param $key_type
+ *   How key/value pairs should be interpreted. Available options:
+ *   - mixed
+ *   - numeric
+ *   - associative
+ *   - custom
+ *   - none
+ */
+function form_options_to_text($options, $key_type) {
+  $output = '';
+  $previous_key = false;
+
+  foreach ($options as $key => $value) {
+    // Convert groups.
+    if (is_array($value)) {
+      $output .= '<' . $key . '>' . "\n";
+      foreach ($value as $subkey => $subvalue) {
+        $output .= (($key_type == 'mixed' || $key_type == 'numeric' || $key_type == 'custom') ? $subkey . '|' : '') . $subvalue . "\n";
+      }
+      $previous_key = $key;
+    }
+    // Typical key|value pairs.
+    else {
+      // Exit out of any groups.
+      if (isset($options[$previous_key]) && is_array($options[$previous_key])) {
+        $output .= "<>\n";
+      }
+      // Skip empty rows.
+      if ($options[$key] !== '') {
+        if ($key_type == 'mixed' || $key_type == 'numeric' || $key_type == 'custom') {
+          $output .= $key . '|' . $value . "\n";
+        }
+        else {
+          $output .= $value . "\n";
+        }
+      }
+      $previous_key = $key;
+    }
+  }
+
+  return $output;
+}
+
+/**
+ * Create an array representation of text option values.
+ *
+ * If the Key of the option is within < >, treat as an optgroup
+ * 
+ * <Group 1>
+ *   creates an optgroup with the label "Group 1"
+ * 
+ * <>
+ *   Exits the current group, allowing items to be inserted at the root element.
+ */
+function form_options_from_text($text, $key_type, $flat = FALSE, &$duplicates = array()) {
+  $keys = array();
+  $items = array();
+  $rows = array_filter(explode("\n", trim($text)));
+  $group = FALSE;
+  foreach ($rows as $row) {
+    $row = trim($row);
+    $matches = array();
+
+    // Check for a simple empty row.
+    if (empty($row)) {
+      continue;
+    }
+    // Check if this row is a group.
+    elseif (!$flat && preg_match('/^\<((([^>|]*)\|)?([^>]*))\>$/', $row, $matches)) {
+      if ($matches[1] === '') {
+        $group = FALSE;
+      }
+      else {
+        $group = $matches[4] ? $matches[4] : '';
+        $keys[] = $group;
+      }
+    }
+    // Check if this row is a key|value pair.
+    elseif (($key_type == 'mixed' || $key_type == 'custom' || $key_type == 'numeric') && preg_match('/^([^|]+)\|(.*)$/', $row, $matches)) {
+      $key = $matches[1];
+      $value = $matches[2];
+      $keys[] = $key;
+      $items[] = array(
+        'key' => $key,
+        'value' => $value,
+        'group' => $group,
+      );
+    }
+    // Set this this row as a straight value.
+    else {
+      $items[] = array(
+        'key' => NULL,
+        'value' => $row,
+        'group' => $group,
+      );
+    }
+  }
+
+  // Expand the list into a nested array, assign keys and check duplicates.
+  $options = array();
+  $new_key = 0;
+  foreach ($items as $item) {
+    // Assign a key if needed.
+    if ($key_type == 'none') {
+      $item['key'] = $new_key++;
+    }
+    elseif (!isset($item['key'])) {
+      while (in_array($new_key, $keys)) {
+        $new_key++;
+      }
+      $keys[] = $new_key;
+      $item['key'] = $new_key;
+    }
+
+    if ($item['group']) {
+      if (isset($options[$item['group']][$item['key']])) {
+        $duplicates[] = $item['key'];
+      }
+      $options[$item['group']][$item['key']] = $item['value'];
+    }
+    else {
+      if (isset($options[$item['key']])) {
+        $duplicates[] = $item['key'];
+      }
+      $options[$item['key']] = $item['value'];
+    }
+  }
+
+  return $options;
+}
+
+/**
+ * Recursive function for finding default value keys. Matches on keys or values.
+ */
+function _form_options_search($needle, $haystack) {
+  if (isset($haystack[$needle])) {
+    return $needle;
+  }
+  foreach ($haystack as $key => $value) {
+    if (is_array($value) && ($return = _form_options_search($needle, $value))) {
+      return $return;
+    }
+    if ($value == $needle) {
+      return $key;
+    }
+  }
+}
+
+/**
  * Theme a submit button form element.
  *
  * @param $variables
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1145
diff -u -r1.1145 common.inc
--- includes/common.inc	7 Apr 2010 17:30:43 -0000	1.1145
+++ includes/common.inc	8 Apr 2010 07:49:34 -0000
@@ -5613,6 +5613,9 @@
     'container' => array(
       'render element' => 'element',
     ),
+    'options' => array(
+      'render element' => 'element',    
+    ),
   );
 }
 
Index: misc/options_element.css
===================================================================
RCS file: misc/options_element.css
diff -N misc/options_element.css
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ misc/options_element.css	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,97 @@
+/* $Id: options_element.css,v 1.5 2010/03/24 19:58:55 quicksketch Exp $ */
+
+div.options-widget .draggable a.tabledrag-handle {
+  padding-right: .3em /* RTL */
+}
+
+div.form-options fieldset.options {
+  margin-bottom: 0;
+}
+
+div.options-widget a.add span,
+div.options-widget a.remove span {
+  display: none;
+}
+
+div.options-widget a.add,
+div.options-widget a.remove,
+div.form-option-add a {
+  display: block;
+  width: 16px;
+  height: 16px;
+  margin: .2em .1em;
+  background-repeat: no-repeat;
+  background-position: 0 0;
+  float: left; /* RTL */
+}
+
+div.options-widget a.add:hover,
+div.options-widget a.remove:hover,
+div.form-option-add a:hover {
+  background-position: 0 -16px;
+}
+
+div.options-widget a.add,
+div.form-option-add a {
+  background-image: url(add.png);
+}
+
+div.options-widget a.remove {
+  background-image: url(delete.png);
+}
+
+div.form-options-manual,
+div.form-option-add {
+  text-align: right; /* RTL */
+}
+div.form-options-manual a,
+div.form-option-add a {
+  float: none;
+  display: inline;
+  padding-left: 20px; /* RTL */
+}
+
+div.options-widget table {
+  width: 100%;
+  margin: 0;
+}
+
+div.options-widget table,
+div.options-widget tbody {
+  border: none;
+}
+
+div.options-widget table tr {
+  border-style: none;
+  background: none;
+}
+
+div.options-widget table td {
+  padding: 4px 6px;
+}
+
+div.options-widget table td.option-default-cell {
+  width: 6em;
+  padding: 4px 0;
+}
+
+div.options-widget table td.option-order-cell {
+  width: 2em;
+}
+
+div.options-widget table tr.indented td.option-order-cell {
+  width: 4em;
+}
+
+div.options-widget table td.option-key-cell {
+  width: 20%;
+}
+
+div.options-widget table td.option-actions-cell {
+  width: 40px;
+}
+
+div.options-widget input.form-text {
+  width: 100%;
+}
+
Index: misc/options_element.js
===================================================================
RCS file: misc/options_element.js
diff -N misc/options_element.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ misc/options_element.js	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,776 @@
+// $Id: options_element.js,v 1.7 2010/03/24 23:53:14 quicksketch Exp $
+
+/**
+ * @file
+ * Add JavaScript behaviors for the "options" form element type.
+ */
+
+(function($) {
+
+Drupal.optionElements = Drupal.optionElements || {};
+Drupal.behaviors.optionsElement = Drupal.behaviors.optionsElement || {};
+
+Drupal.behaviors.optionsElement.attach = function(context) {
+  $('div.form-options:not(.options-element-processed)', context).each(function() {
+    $(this).addClass('options-element-processed');
+    var optionsElement = new Drupal.optionsElement(this);
+    Drupal.optionElements[optionsElement.identifier] = optionsElement;
+  });
+};
+
+/**
+ * Constructor for an options element.
+ */
+Drupal.optionsElement = function(element) {
+  var self = this;
+
+  // Find the original "manual" fields.
+  this.element = element;
+  this.manualElement = $(element).find('fieldset.options').get(0);
+  this.manualOptionsElement = $(element).find('textarea').get(0);
+  this.manualDefaultValueElement = $(element).find('input.form-text').get(0);
+  this.keyTypeToggle = $(element).find('input.key-type-toggle').get(0);
+  this.multipleToggle = $(element).find('input.multiple-toggle').get(0);
+
+  // Setup variables containing the current status of the widget.
+  this.optgroups = $(element).is('.options-optgroups');
+  this.multiple = $(element).is('.options-multiple');
+  this.keyType = element.className.replace(/^.*?options-key-type-([a-z]+).*?$/, '$1');
+  this.customKeys = Boolean(element.className.match(/options-key-custom/));
+  this.identifier = this.manualOptionsElement.id + '-widget';
+  this.enabled = $(this.manualOptionsElement).attr('readonly') == '';
+
+  // Warning messages.
+  this.keyChangeWarning = Drupal.t('Custom keys have been specified in this list. Removing these custom keys may change way data is stored. Are you sure you wish to remove these custom keys?');
+
+  // Setup new DOM elements containing the actual options widget.
+  this.optionsElement = $('<div></div>').get(0); // Temporary DOM object.
+  this.optionsToggleElement = $(Drupal.theme('optionsElementToggle')).get(0);
+  this.optionAddElement = $(Drupal.theme('optionsElementAdd')).get(0);
+
+  // Add the options widget and toggle elements to the page.
+  $(this.manualElement).css('display', 'none').before(this.optionsElement).after(this.optionsToggleElement).after(this.optionAddElement);
+
+  // Enable add item link.
+  $(this.optionAddElement).find('a').click(function() {
+    self.addOption($('table tr:last', self.optionsElement).get(0));
+    return false;
+  });
+
+  // Enable the toggle action for manual entry of options.
+  $(this.optionsToggleElement).find('a').click(function() {
+    self.toggleMode();
+    return false;
+  });
+
+  // Add a handler for key type changes.
+  if (this.keyTypeToggle) {
+    $(this.keyTypeToggle).click(function() {
+      var checked = $(this).attr('checked');
+      // Before switching the key type, ensure we're not destroying user keys.
+      if (!checked) {
+        var options = self.optionsFromText();
+        var confirm = false;
+        if (self.keyType == 'associative') {
+          for (var n in options) {
+            if (options[n].key != options[n].value) {
+              confirm = true;
+              break;
+            }
+          }
+        }
+        if (confirm) {
+          if (window.confirm(self.keyChangeWarning)) {
+            self.setCustomKeys(false);
+          }
+        }
+        else {
+          self.setCustomKeys(false);
+        }
+      }
+      else {
+        self.setCustomKeys(true);
+      }
+    });
+  }
+
+  // Add a handler for multiple value changes.
+  if (this.multipleToggle) {
+    $(this.multipleToggle).click(function(){
+      self.setMultiple($(this).attr('checked'));
+    });
+  }
+
+  // Be sure to show the custom keys if we have any errors.
+  if (Drupal.settings.optionsElement && Drupal.settings.optionsElement.errors) {
+    this.customKeys = true;
+  }
+
+  // Update the options widget with the current state of the textarea.
+  this.updateWidgetElements();
+
+  // Highlight errors that may have occurred during Drupal validation.
+  if (Drupal.settings.optionsElement && Drupal.settings.optionsElement.errors) {
+    this.checkKeys(Drupal.settings.optionsElement.errors, 'error');
+  }
+}
+
+/**
+ * Update the widget element based on the current values of the manual elements.
+ */
+Drupal.optionsElement.prototype.updateWidgetElements = function() {
+  var self = this;
+
+  // Create a new options element and replace the existing one.
+  var newElement = $(Drupal.theme('optionsElement', this)).get(0);
+  if ($(this.optionsElement).css('display') == 'none') {
+    $(newElement).css('display', 'none');
+  }
+  $(this.optionsElement).replaceWith(newElement);
+  this.optionsElement = newElement;
+
+  // Manually set up table drag for the created table.
+  Drupal.settings.tableDrag = Drupal.settings.tableDrag || {};
+  Drupal.settings.tableDrag[this.identifier] = {
+    'option-depth': {
+      0: {
+        action: 'depth',
+        hidden: false,
+        limit: 0,
+        relationship: 'self',
+        source: 'option-depth',
+        target: 'option-depth'
+      }
+    }
+  };
+
+  // Allow indentation of elements if optgroups are supported.
+  if (this.optgroups) {
+    Drupal.settings.tableDrag[this.identifier]['option-parent'] = {
+      0: {
+        action: 'match',
+        hidden: false,
+        limit: 1,
+        relationship: 'parent',
+        source: 'option-value',
+        target: 'option-parent'
+      }
+    };
+  }
+
+  // Enable button for adding options.
+  $('a.add', this.optionsElement).click(function() {
+    var newOption = self.addOption($(this).parents('tr:first').get(0));
+    $(newOption).find('a.add').focus();
+    return false;
+  });
+
+  // Enable button for removing options.
+  $('a.remove', this.optionsElement).click(function() {
+    self.removeOption($(this).parents('tr:first').get(0));
+    return false;
+  });
+
+  // Add the same update action to all textfields and radios.
+  $('input', this.optionsElement).change(function() {
+    self.updateOptionElements();
+    self.updateManualElements();
+  });
+
+  // Add a delayed update to textfields.
+  $('input.option-value', this.optionsElement).keyup(function(e) {
+    self.pendingUpdate(e);
+  });
+
+  // Attach behaviors as normal to the new widget.
+  Drupal.attachBehaviors(this.optionsElement);
+
+  // Add an onDrop action to the table drag instance.
+  Drupal.tableDrag[this.identifier].onDrop = function() {
+    // Update the checkbox/radio buttons for selecting default values.
+    if (self.optgroups) {
+      self.updateOptionElements();
+    }
+    // Update the options within the hidden text area.
+    self.updateManualElements();
+  };
+
+  // Add an onIndent action to the table drag row instances.
+  Drupal.tableDrag[this.identifier].row.prototype.onIndent = function() {
+    if (this.indents) {
+      $(this.element).addClass('indented');
+    }
+    else {
+      $(this.element).removeClass('indented');
+    }
+  };
+
+  // Update the default value and optgroups.
+  this.updateOptionElements();
+}
+
+/**
+ * Update the original form element based on the current state of the widget.
+ */
+Drupal.optionsElement.prototype.updateManualElements = function() {
+  var options = {};
+
+  // Build a list of current options.
+  var previousOption = false;
+  $(this.optionsElement).find('input.option-value').each(function() {
+    var $row = $(this).is('tr') ? $(this) : $(this).parents('tr:first');
+    var depth = $row.find('input.option-depth').val();
+    if (depth == 1 && previousOption) {
+      if (typeof(options[previousOption]) != 'object') {
+        options[previousOption] = {};
+      }
+      options[previousOption][this.value] = this.value;
+    }
+    else {
+      options[this.value] = this.value;
+      previousOption = this.value;
+    }
+  });
+  this.options = options;
+
+  // Update the default value.
+  var defaultValue = this.multiple ? [] : '';
+  var multiple = this.multiple;
+  $(this.optionsElement).find('input.option-default').each(function() {
+    if (this.checked && this.value) {
+      if (multiple) {
+        defaultValue.push(this.value);
+      }
+      else {
+        defaultValue = this.value;
+      }
+    }
+  });
+  this.defaultValue = defaultValue;
+
+  // Update with the new text and trigger the change action on the field.
+  this.optionsToText();
+  if (this.manualDefaultValueElement) {
+    this.manualDefaultValueElement.value = multiple ? defaultValue.join(', ') : defaultValue;
+  }
+
+  $(this.manualOptionsElement).change();
+}
+
+/**
+ * Several maintenance routines to update all rows of the options element.
+ *
+ * - Disable options for optgroups if indented.
+ * - Disable add and delete links if indented.
+ * - Match the default value radio button value to the key of the text element.
+ */
+Drupal.optionsElement.prototype.updateOptionElements = function() {
+  var self = this;
+  var previousRow = false;
+  var previousElement = false;
+  var $rows = $(this.optionsElement).find('tbody tr');
+
+  $rows.each(function(index) {
+    var optionValue = $(this).find('input.option-value').val();
+    var optionKey = $(this).find('input.option-key').val();
+
+    // Update the elements key if matching the key and value.
+    if (self.keyType == 'associative') {
+      $(this).find('input.option-key').val(optionValue);
+    }
+
+    // Match the default value checkbox/radio button to the option's key.
+    $(this).find('input.option-default').val(optionKey ? optionKey : optionValue);
+
+    // Hide the add/remove links the row if indented.
+    var depth = $(this).find('input.option-depth').val();
+    var defaultInput = $(this).find('input.option-default').get(0);
+
+    if (depth == 1) {
+      // Affect the parent row, adjusting properties for optgroup items.
+      $(previousElement).attr('disabled', true).attr('checked', false);
+      $(previousRow).addClass('optgroup').find('a.add, a.remove').css('display', 'none');
+      $(this).find('a.add, a.remove').css('display', '');
+      $(defaultInput).attr('disabled', false);
+
+      // Hide the key column for the optgroup. It would be nice if hiding
+      // columns worked in IE7, but for now this only works in IE8 and other
+      // standards-compliant browsers.
+      if (self.customKeys && (!$.browser.msie || $.browser.version >= 8)) {
+        $(previousRow).find('td.option-key-cell').css('display', 'none');
+        $(previousRow).find('td.option-value-cell').attr('colspan', 2);
+      }
+    }
+    else {
+      // Set properties for normal options that are not optgroups.
+      $(defaultInput).attr('disabled', false);
+      $(this).removeClass('optgroup').find('a.add, a.remove').css('display', '');
+
+      // Hide the key column. See note above for compatibility concerns.
+      if (self.customKeys && (!$.browser.msie || $.browser.version >= 8)) {
+        $(this).find('td.option-key-cell').css('display', '');
+        $(this).find('td.option-value-cell').attr('colspan', '');
+      }
+      previousElement = defaultInput;
+      previousRow = this;
+    }
+  });
+
+  // Do not allow the last item to be removed.
+  if ($rows.size() == 1) {
+    $rows.find('a.remove').css('display', 'none')
+  }
+
+  // Disable items if needed.
+  if (this.enabled == false) {
+    this.disable();
+  }
+}
+
+/**
+ * Add a new option below the current row.
+ */
+Drupal.optionsElement.prototype.addOption = function(currentOption) {
+  var self = this;
+  var windowHieght = $(document).height();
+  var newOption = $(currentOption).clone()
+    .find('input.option-key').val(self.keyType == 'numeric' ? self.nextNumericKey() : '').end()
+    .find('input.option-value').val('').end()
+    .find('input.option-default').attr('checked', false).end()
+    .find('a.tabledrag-handle').remove().end()
+    .removeClass('drag-previous')
+    .insertAfter(currentOption)
+    .get(0);
+
+  // Scroll down to accomidate the new option.
+  $(window).scrollTop($(window).scrollTop() + $(document).height() - windowHieght);
+
+  // Make the new option draggable.
+  Drupal.tableDrag[this.identifier].makeDraggable(newOption);
+
+  // Enable button for adding options.
+  $('a.add', newOption).click(function() {
+    var newOption = self.addOption($(this).parents('tr:first').get(0));
+    $(newOption).find('a.add').focus();
+    return false;
+  });
+
+  // Enable buttons for removing options.
+  $('a.remove', newOption).click(function() {
+    self.removeOption(newOption);
+    return false;
+  });
+
+  // Add the update action to all textfields and radios.
+  $('input', newOption).change(function() {
+    self.updateOptionElements();
+    self.updateManualElements();
+  });
+
+  // Add a delayed update to textfields.
+  $('input.option-value', newOption).keyup(function(e) {
+    self.pendingUpdate(e);
+  });
+
+  this.updateOptionElements();
+  this.updateManualElements();
+
+  return newOption;
+}
+
+/**
+ * Remove the current row.
+ */
+Drupal.optionsElement.prototype.removeOption = function(currentOption) {
+  $(currentOption).remove();
+
+  this.updateOptionElements();
+  this.updateManualElements();
+}
+
+/**
+ * Toggle link for switching between the JavaScript and manual entry.
+ */
+Drupal.optionsElement.prototype.toggleMode = function() {
+  if ($(this.optionsElement).is(':visible')) {
+    var height = $(this.optionsElement).height();
+    $(this.optionsElement).css('display', 'none');
+    $(this.optionAddElement).css('display', 'none');
+    $(this.manualElement).css('display', '').find('textarea').height(height);
+    $(this.optionsToggleElement).find('a').text(Drupal.t('Normal entry'));
+  }
+  else {
+    this.updateWidgetElements();
+    $(this.optionsElement).css('display', '');
+    $(this.optionAddElement).css('display', '');
+    $(this.manualElement).css('display', 'none');
+    $(this.optionsToggleElement).find('a').text(Drupal.t('Manual entry'));
+  }
+}
+
+/**
+ * Enable the changing of options.
+ */
+Drupal.optionsElement.prototype.enable = function() {
+  this.enabled = true;
+  $(this.manualOptionsElement).attr('readonly', '');
+  $(this.element).removeClass('options-disabled');
+
+  $('a.add, a.remove, a.tabledrag-handle, div.form-option-add a', this.element).css('display', '');
+  $('input.form-text', this.optionsElement).attr('disabled', '');
+};
+
+/**
+ * Disable the changing of options.
+ */
+Drupal.optionsElement.prototype.disable = function() {
+  this.enabled = false;
+  $(this.manualOptionsElement).attr('readonly', true);
+  $(this.element).addClass('options-disabled');
+
+  $('a.add, a.remove, a.tabledrag-handle, div.form-option-add a', this.element).css('display', 'none');
+  $('input.form-text', this.optionsElement).attr('disabled', 'disabled');
+};
+
+/**
+ * Enable entering of custom key values.
+ */
+Drupal.optionsElement.prototype.setCustomKeys = function(enabled) {
+  if (enabled) {
+    $(this.element).addClass('options-key-custom');
+  }
+  else {
+    $(this.element).removeClass('options-key-custom');
+  }
+
+  this.customKeys = enabled;
+  // Rebuild the options widget.
+  this.updateManualElements();
+  this.updateWidgetElements();
+}
+
+/**
+ * Change the current key type (associative, custom, numeric, none).
+ */
+Drupal.optionsElement.prototype.setKeyType = function(type) {
+  $(this.element)
+    .removeClass('options-key-type-' + this.keyType)
+    .addClass('options-key-type-' + type);
+  this.keyType = type;
+  // Rebuild the options widget.
+  this.updateManualElements();
+  this.updateWidgetElements();
+}
+
+/**
+ * Set the element's #multiple property. Boolean TRUE or FALSE.
+ */
+Drupal.optionsElement.prototype.setMultiple = function(multiple) {
+  if (multiple) {
+    $(this.element).addClass('options-multiple');
+  }
+  else {
+    // Unselect all default options except the first.
+    $(this.optionsElement).find('input.option-default:checked:not(:first)').attr('checked', false);
+    this.updateManualElements();
+    $(this.element).removeClass('options-multiple');
+  }
+  this.multiple = multiple;
+  // Rebuild the options widget.
+  this.updateWidgetElements();
+};
+
+/**
+ * Highlight duplicate keys.
+ */
+Drupal.optionsElement.prototype.checkKeys = function(duplicateKeys, cssClass){
+  $(this.optionsElement).find('input.option-key').each(function() {
+    if (duplicateKeys[this.value]) {
+      $(this).addClass(cssClass);
+    }
+  });
+};
+
+/**
+ * Update a field after a delay.
+ *
+ * Similar to immediately changing a field, this field as pending changes that
+ * will be updated after a delay. This includes textareas and textfields in
+ * which updating continuously would be a strain the server and actually slow
+ * down responsiveness.
+ */
+Drupal.optionsElement.prototype.pendingUpdate = function(e) {
+  var self = this;
+
+  // Only operate on "normal" keys, excluding special function keys.
+  // http://protocolsofmatrix.blogspot.com/2007/09/javascript-keycode-reference-table-for.html
+  if (!(
+    e.keyCode >= 48 && e.keyCode <= 90 || // 0-9, A-Z.
+    e.keyCode >= 93 && e.keyCode <= 111 || // Number pad.
+    e.keyCode >= 186 && e.keyCode <= 222 || // Symbols.
+    e.keyCode == 8) // Backspace.
+    ) {
+    return;
+  }
+
+  if (this.updateDelay) {
+    clearTimeout(this.updateDelay);
+  }
+
+  this.updateDelay = setTimeout(function(){
+    self.updateOptionElements();
+    self.updateManualElements();
+  }, 500);
+};
+
+/**
+ * Given an object of options, convert it to a text string.
+ */
+Drupal.optionsElement.prototype.optionsToText = function() {
+  var $rows = $('tbody tr', this.optionsElement);
+  var output = '';
+  var inGroup = false;
+  var rowCount = $rows.size();
+  var defaultValues = [];
+
+  for (var rowIndex = 0; rowIndex < rowCount; rowIndex++) {
+    var isOptgroup = $rows.eq(rowIndex).is('.optgroup');
+    var isChild = $rows.eq(rowIndex).is('.indented');
+    var key = $rows.eq(rowIndex).find('input.option-key').val();
+    var value = $rows.eq(rowIndex).find('input.option-value').val();
+
+    // Handle groups.
+    if (this.optgroups && value !== '' && isOptgroup) {
+      output += '<' + ((key !== '') ? (key + '|') : '') + value + '>' + "\n";
+      inGroup = true;
+    }
+    // Typical key|value pairs.
+    else {
+      // Exit out of any groups.
+      if (this.optgroups && inGroup && !isChild) {
+        output += "<>\n";
+        inGroup = false;
+      }
+
+      // Add the row for the option.
+      if (this.keyType == 'none' || this.keyType == 'associative') {
+        output += value + "\n";
+      }
+      else if (value == '') {
+        output += "\n";
+      }
+      else {
+        output += ((key !== '') ? (key + '|') : '') + value + "\n";
+      }
+    }
+  }
+
+  this.manualOptionsElement.value = output;
+};
+
+/**
+ * Given a text string, convert it to an object.
+ */
+Drupal.optionsElement.prototype.optionsFromText = function() {
+  // Use jQuery val() instead of value because it fixes Windows line breaks.
+  var rows = $(this.manualOptionsElement).val().match(/^.*$/mg);
+  var parent = '';
+  var options = [];
+  var defaultValues = {};
+
+  // Drop the last row if empty.
+  if (rows.length && rows[rows.length - 1] == '') {
+    rows.pop();
+  }
+
+  if (this.manualDefaultValueElement) {
+    if (this.multiple) {
+      var defaults = this.manualDefaultValueElement.value.split(',');
+      for (var n in defaults) {
+        var defaultValue = defaults[n].replace(/^[ ]*(.*?)[ ]*$/, '$1'); // trim().
+        defaultValues[defaultValue] = defaultValue;
+      }
+    }
+    else {
+      var defaultValue = this.manualDefaultValueElement.value.replace(/^[ ]*(.*?)[ ]*$/, '$1'); // trim().
+      defaultValues[defaultValue] = defaultValue;
+    }
+  }
+
+  for (var n = 0; n < rows.length; n++) {
+    var row = rows[n].replace(/^[ \r\n]*(.*?)[ \r\n]*$/, '$1'); // trim().
+    var key = '';
+    var value = '';
+    var checked = false;
+    var hasChildren = false;
+    var groupClear = false;
+
+    var matches = {};
+    // Row is a group.
+    if (this.optgroups && (matches = row.match(/^\<((([^>|]*)\|)?([^>]*))\>$/))) {
+      if (matches[0] == '<>') {
+        parent = '';
+        groupClear = true;
+      }
+      else {
+        key = matches[3] ? matches[3] : '';
+        parent = value = matches[4];
+        hasChildren = true;
+      }
+    }
+    // Row is a key|value pair.
+    else if ((this.keyType == 'mixed' || this.keyType == 'numeric' || this.keyType == 'custom') && (matches = row.match(/^([^|]+)\|(.*)$/))) {
+      key = matches[1];
+      value = matches[2];
+      checked = defaultValues[key];
+    }
+    // Row is a straight value.
+    else {
+      key = (this.keyType == 'mixed' || this.keyType == 'numeric') ? '' : row;
+      value = row;
+      if (!key && this.keyType == 'mixed') {
+        checked = defaultValues[value];
+      }
+      else {
+        checked = defaultValues[key];
+      }
+    }
+
+    if (!groupClear) {
+      options.push({
+        key: key,
+        value: value,
+        parent: (value !== parent ? parent : ''),
+        hasChildren: hasChildren,
+        checked: (checked ? 'checked' : false)
+      });
+    }
+  }
+
+  // Convert options to numeric if no key is specified.
+  if (this.keyType == 'numeric') {
+    var nextKey = this.nextNumericKey();
+    for (var n in options) {
+      if (options[n].key == '') {
+        options[n].key = nextKey;
+        nextKey++;
+      }
+    }
+  }
+
+  return options;
+};
+
+/**
+ * Utility method to get the next numeric option in a list of options.
+ */
+Drupal.optionsElement.prototype.nextNumericKey = function(options) {
+  this.keyType = 'custom';
+  options = this.optionsFromText();
+  this.keyType = 'numeric';
+
+  var maxKey = -1;
+  for (var n in options) {
+    if (options[n].key.match(/^[0-9]+$/)) {
+      maxKey = Math.max(maxKey, options[n].key);
+    }
+  }
+  return maxKey + 1;
+};
+
+/**
+ * Theme function for creating a new options element.
+ *
+ * @param optionsElement
+ *   An options element object.
+ */
+Drupal.theme.prototype.optionsElement = function(optionsElement) {
+  var output = '';
+  var options = optionsElement.optionsFromText();
+  var hasDefault = optionsElement.manualDefaultValueElement;
+  var defaultType = optionsElement.multiple ? 'checkbox' : 'radio';
+  var keyType = optionsElement.customKeys ? 'textfield' : 'hidden';
+
+  // Helper function to print out a single draggable option row.
+  function tableDragRow(key, value, parent, indent, status) {
+    var output = '';
+    output += '<tr class="draggable' + (indent > 0 ? ' indented' : '') + '">'
+    output += '<td class="' + (hasDefault ? 'option-default-cell' : 'option-order-cell') + '">';
+    for (var n = 0; n < indent; n++) {
+      output += Drupal.theme('tableDragIndentation');
+    }
+    output += '<input type="hidden" class="option-parent" value="' + parent + '" />';
+    output += '<input type="hidden" class="option-depth" value="' + indent + '" />';
+    if (hasDefault) {
+      output += '<input type="' + defaultType + '" name="' + optionsElement.identifier + '-default" class="form-radio option-default" value="' + key + '"' + (status == 'checked' ? ' checked="checked"' : '') + (status == 'disabled' ? ' disabled="disabled"' : '') + ' />';
+    }
+    output += '</td><td class="' + (keyType == 'textfield' ? 'option-key-cell' : 'option-value-cell') +'">';
+    output += '<input type="' + keyType + '" class="' + (keyType == 'textfield' ? 'form-text ' : '') + 'option-key" value="' + key + '" />';
+    output += keyType == 'textfield' ? '</td><td class="option-value-cell">' : '';
+    output += '<input class="form-text option-value" type="text" value="' + value + '" />';
+    output += '</td><td class="option-actions-cell">'
+    output += '<a class="add" title="' + Drupal.t('Add new option') + '" href="#"' + (status == 'disabled' ? ' style="display: none"' : '') + '><span class="add">' + Drupal.t('Add') + '</span></a>';
+    output += '<a class="remove" title="' + Drupal.t('Remove option') + '" href="#"' + (status == 'disabled' ? ' style="display: none"' : '') + '><span class="remove">' + Drupal.t('Remove') + '</span></a>';
+    output += '</td>';
+    output += '</tr>';
+    return output;
+  }
+
+  output += '<div class="options-widget">';
+  output += '<table id="' + optionsElement.identifier + '">';
+
+  output += '<thead><tr>';
+  output += '<th>' + (hasDefault ? Drupal.t('Default') : '&nbsp;') + '</th>';
+  output += keyType == 'textfield' ? '<th>' + Drupal.t('Key') + '</th>' : '';
+  output += '<th>' + Drupal.t('Value') + '</th>';
+  output += '<th>&nbsp;</th>';
+  output += '</tr></thead>';
+
+  output += '<tbody>';
+
+  // Make sure that at least a few options exist if empty.
+  if (!options.length) {
+    var newOption = {
+      key: '',
+      value: '',
+      parent: '',
+      hasChildren: false,
+      checked: false
+    }
+    options.push(newOption);
+    options.push(newOption);
+    options.push(newOption);
+  }
+
+  for (var n in options) {
+    var option = options[n];
+    var depth = option.parent === '' ? 0 : 1;
+    var checked = !option.hasChildren && option.checked;
+    output += tableDragRow(option.key, option.value, option.parent, depth, checked);
+  }
+
+  output += '</tbody>';
+  output += '</table>';
+  output += '<div>';
+
+  return output;
+};
+
+Drupal.theme.prototype.optionsElementAdd = function() {
+  return '<div class="form-option-add"><a href="#">' + Drupal.t('Add item') + '</a></div>';
+};
+
+Drupal.theme.prototype.optionsElementToggle = function() {
+  return '<div class="form-options-manual"><a href="#">' + Drupal.t('Manual entry') + '</a></div>';
+};
+
+Drupal.theme.tableDragChangedMarker = function () {
+  return ' ';
+};
+
+Drupal.theme.tableDragChangedWarning = function() {
+  return '<span></span>';
+};
+
+})(jQuery);
+

