Index: includes/ajax.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/ajax.inc,v
retrieving revision 1.6
diff -u -p -r1.6 ajax.inc
--- includes/ajax.inc	24 Aug 2009 00:14:18 -0000	1.6
+++ includes/ajax.inc	26 Aug 2009 18:11:24 -0000
@@ -149,7 +149,7 @@ function ajax_render($commands = array()
  */
 function ajax_render_error($error = '') {
   $commands = array();
-  $commands[] = ajax_command_error(empty($error) ? t('An error occurred while handling the request: The server received invalid input.') : $error);
+  $commands[] = ajax_command_alert(empty($error) ? t('An error occurred while handling the request: The server received invalid input.') : $error);
   ajax_render($commands);
 }
 
@@ -301,7 +301,7 @@ function ajax_process_form($element) {
       'wrapper'  => empty($element['#ajax']['wrapper']) ? NULL : $element['#ajax']['wrapper'],
       'selector' => empty($element['#ajax']['selector']) ? '#' . $element['#id'] : $element['#ajax']['selector'],
       'effect'   => empty($element['#ajax']['effect']) ? 'none' : $element['#ajax']['effect'],
-      'speed '   => empty($element['#ajax']['effect']) ? 'none' : $element['#ajax']['effect'],
+      'speed'    => empty($element['#ajax']['effect']) ? 'none' : $element['#ajax']['effect'],
       'method'   => empty($element['#ajax']['method']) ? 'replace' : $element['#ajax']['method'],
       'progress' => empty($element['#ajax']['progress']) ? array('type' => 'throbber') : $element['#ajax']['progress'],
       'button'   => isset($element['#executes_submit_callback']) ? array($element['#name'] => $element['#value']) : FALSE,
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.366
diff -u -p -r1.366 form.inc
--- includes/form.inc	25 Aug 2009 21:16:31 -0000	1.366
+++ includes/form.inc	26 Aug 2009 18:09:33 -0000
@@ -155,6 +155,14 @@ function drupal_build_form($form_id, &$f
       $form_build_id = 'form-' . md5(uniqid(mt_rand(), TRUE));
       $form['#build_id'] = $form_build_id;
 
+      // Record the filepath of the include file containing the original form, so
+      // the form builder callbacks are found when the form is being rebuilt on a
+      // different path (such as 'system/ajax').
+      $item = menu_get_item();
+      if (!empty($item['file'])) {
+        $form_state['include_file'] = $form['#include_file'] = $item['file'];
+      }
+
       // Fix the form method, if it is 'get' in $form_state, but not in $form.
       if ($form_state['method'] == 'get' && !isset($form['#method'])) {
         $form['#method'] = 'get';
@@ -299,6 +307,13 @@ function drupal_rebuild_form($form_id, &
 function form_get_cache($form_build_id, &$form_state) {
   if ($cached = cache_get('form_' . $form_build_id, 'cache_form')) {
     $form = $cached->data;
+    // If drupal_build_form() recorded an optional include file containing the
+    // original form, load it.
+    if (!empty($form['#include_file']) && file_exists($form['#include_file'])) {
+      require_once DRUPAL_ROOT . '/' . $form['#include_file'];
+      $form_state['include_file'] = $form['#include_file'];
+    }
+
     global $user;
     if ((isset($form['#cache_token']) && drupal_valid_token($form['#cache_token'])) || (!isset($form['#cache_token']) && !$user->uid)) {
       if ($cached = cache_get('storage_' . $form_build_id, 'cache_form')) {
@@ -452,6 +467,11 @@ function drupal_retrieve_form($form_id, 
   $form = call_user_func_array(isset($callback) ? $callback : $form_id, $args);
   $form['#form_id'] = $form_id;
   $form['#args'] = $form_state['args'];
+
+  if (!empty($form_state['include_file'])) {
+    $form['#include_file'] = $form_state['include_file'];
+  }
+
   return $form;
 }
 
Index: modules/field/field.form.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v
retrieving revision 1.21
diff -u -p -r1.21 field.form.inc
--- modules/field/field.form.inc	24 Aug 2009 21:00:23 -0000	1.21
+++ modules/field/field.form.inc	26 Aug 2009 15:25:59 -0000
@@ -141,8 +141,7 @@ function field_multiple_value_form($fiel
   $title = check_plain(t($instance['label']));
   $description = field_filter_xss(t($instance['description']));
 
-  $bundle_name_url_css = str_replace('_', '-', $instance['bundle']);
-  $field_name_url_css = str_replace('_', '-', $field_name);
+  $wrapper_id = str_replace('_', '-', $field_name) . '-wrapper';
 
   $form_element = array(
     '#theme' => 'field_multiple_value_form',
@@ -150,8 +149,9 @@ function field_multiple_value_form($fiel
     '#title' => $title,
     '#required' => $instance['required'],
     '#description' => $description,
-    '#prefix' => '<div id="' . $field_name_url_css . '-wrapper">',
+    '#prefix' => '<div id="' . $wrapper_id . '">',
     '#suffix' => '</div>',
+    '#max_delta' => $max,
   );
 
   $function = $instance['widget']['module'] . '_field_widget';
@@ -188,28 +188,25 @@ function field_multiple_value_form($fiel
       }
     }
 
-    // Add AHAH add more button, if not working with a programmed form.
+    // Add 'add more' button, if not working with a programmed form.
     if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED && empty($form_state['programmed'])) {
-      $bundle_name_url_str = str_replace('_', '-', $instance['bundle']);
-      $field_name_url_str = str_replace('_', '-', $field_name);
 
       $form_element[$field_name . '_add_more'] = array(
         '#type' => 'submit',
         '#name' => $field_name . '_add_more',
         '#value' => t('Add another item'),
+        '#attributes' => array('class' => array('field-add-more-submit')),
         // Submit callback for disabled JavaScript.
         '#submit' => array('field_add_more_submit'),
         '#ajax' => array(
-          'path' => 'field/js_add_more/' . $bundle_name_url_css . '/' . $field_name_url_css,
-          'wrapper' => $field_name_url_css . '-wrapper',
+          'callback' => 'field_add_more_js',
+          'wrapper' => $wrapper_id,
           'method' => 'replace',
           'effect' => 'fade',
         ),
-        // When JS is disabled, the field_add_more_submit handler will find
-        // the relevant field using these entries.
+        // The field_add_more_submit() and field_add_more_js() handlers will
+        // find the relevant field using those entries.
         '#field_name' => $field_name,
-        '#bundle' => $instance['bundle'],
-        '#attributes' => array('class' => array('field-add-more-submit')),
         '#language' => $langcode,
       );
     }
@@ -343,114 +340,27 @@ function field_add_more_submit($form, &$
 }
 
 /**
- * Menu callback for AHAH addition of new empty widgets.
+ * Ajax callback for addition of new empty widgets.
  */
-function field_add_more_js($bundle_name, $field_name) {
-  // Arguments are coming from the url, so we translate back dashes.
-  $field_name = str_replace('-', '_', $field_name);
-
-  $invalid = FALSE;
-  if (empty($_POST['form_build_id'])) {
-    // Invalid request.
-    $invalid = TRUE;
-  }
-
-  // Retrieve the cached form.
-  $form_state = form_state_defaults();
-  $form_build_id = $_POST['form_build_id'];
-  $form = form_get_cache($form_build_id, $form_state);
-  if (!$form) {
-    // Invalid form_build_id.
-    $invalid = TRUE;
-  }
-
+function field_add_more_js($form, $form_state) {
   // Retrieve field information.
+  $field_name = $form_state['clicked_button']['#field_name'];
   $field = $form['#fields'][$field_name]['field'];
-  $instance = $form['#fields'][$field_name]['instance'];
-  $form_path = $form['#fields'][$field_name]['form_path'];
   if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) {
-    // Ivnalid
-    $invalid = TRUE;
-  }
-
-  if ($invalid) {
     ajax_render(array());
   }
-
-  // We don't simply return a new empty widget row to append to existing ones,
-  // because:
-  // - ahah.js won't simply let us add a new row to a table
-  //   @todo ajax.js lets you. :)
-  // - attaching the 'draggable' behavior won't be easy
-  // So we resort to rebuilding the whole table of widgets including the
-  // existing ones, which makes us jump through a few hoops.
-
-  // The form that we get from the cache is unbuilt. We need to build it so
-  // that _value callbacks can be executed and $form_state['values'] populated.
-  // We only want to affect $form_state['values'], not the $form itself
-  // (built forms aren't supposed to enter the cache) nor the rest of
-  // $form_state, so we use copies of $form and $form_state.
-  $form_copy = $form;
-  $form_state_copy = $form_state;
-  $form_state_copy['input'] = array();
-  form_builder($_POST['form_id'], $form_copy, $form_state_copy);
-  // Just grab the data we need.
-  $form_state['values'] = $form_state_copy['values'];
-  // Reset cached ids, so that they don't affect the actual form we output.
-  drupal_static_reset('form_clean_id');
-
-  // Ensure that a valid language is provided.
-  $langcode = key($_POST[$field_name]);
-  if ($langcode != FIELD_LANGUAGE_NONE) { 
-    $langcode = field_multilingual_valid_language($langcode);
-  }
-
-  // Sort the $form_state['values'] we just built *and* the incoming $_POST data
-  // according to d-n-d reordering.
-  unset($form_state['values'][$field_name][$langcode][$field['field_name'] . '_add_more']);
-  foreach ($_POST[$field_name][$langcode] as $delta => $item) {
-    $form_state['values'][$field_name][$langcode][$delta]['_weight'] = $item['_weight'];
-  }
-  $form_state['values'][$field_name][$langcode] = _field_sort_items($field, $form_state['values'][$field_name][$langcode]);
-  $_POST[$field_name][$langcode] = _field_sort_items($field, $_POST[$field_name][$langcode]);
-
-  // Build our new form element for the whole field, asking for one more element.
-  $form_state['field_item_count'] = array($field_name => count($_POST[$field_name][$langcode]) + 1);
-  $items = $form_state['values'][$field_name][$langcode];
-  $form_element = field_default_form(NULL, NULL, $field, $instance, $langcode, $items, $form, $form_state);
-  // Let other modules alter it.
-  drupal_alter('form', $form_element, array(), 'field_add_more_js');
-
-  // Add the new element at the right location in the (original, unbuilt) form.
-  $target = &$form;
-  foreach ($form_path as $key) {
-    $target = &$target[$key];
-  }
-  $target = $form_element[$field_name];
-
-  // Save the new definition of the form.
-  $form_state['values'] = array();
-  form_set_cache($form_build_id, $form, $form_state);
-
-  // Build the new form against the incoming $_POST values so that we can
-  // render the new element.
-  $delta = max(array_keys($_POST[$field_name][$langcode])) + 1;
-  $_POST[$field_name][$langcode][$delta]['_weight'] = $delta;
-  $form_state = form_state_defaults();
-  $form_state['input'] = $_POST;
-  $form = form_builder($_POST['form_id'], $form, $form_state);
-
-  // Render the new output.
-  // We fetch the form element from the built $form.
+  // Navigate to the right part of the form.
+  $form_path = $form['#fields'][$field_name]['form_path'];
   $field_form = $form;
   foreach ($form_path as $key) {
     $field_form = $field_form[$key];
   }
+  $field_form = $field_form[$field_form['#language']];
+
   // Add a DIV around the new field to receive the AJAX effect.
+  $delta = $field_form['#max_delta'];
   $field_form[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($field_form[$delta]['#prefix']) ? $field_form[$delta]['#prefix'] : '');
   $field_form[$delta]['#suffix'] = (isset($field_form[$delta]['#suffix']) ? $field_form[$delta]['#suffix'] : '') . '</div>';
-  // Prevent duplicate wrapper.
-  unset($field_form['#prefix'], $field_form['#suffix']);
 
   $output = theme('status_messages') . drupal_render($field_form);
 
Index: modules/field/field.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.module,v
retrieving revision 1.28
diff -u -p -r1.28 field.module
--- modules/field/field.module	24 Aug 2009 00:14:20 -0000	1.28
+++ modules/field/field.module	26 Aug 2009 15:25:59 -0000
@@ -156,23 +156,6 @@ function field_init() {
 }
 
 /**
- * Implement hook_menu().
- */
-function field_menu() {
-  $items = array();
-
-  // Callback for AHAH add more buttons.
-  $items['field/js_add_more'] = array(
-    'page callback' => 'field_add_more_js',
-    'access arguments' => array('access content'),
-    'type' => MENU_CALLBACK,
-    'file' => 'field.form.inc',
-  );
-
-  return $items;
-}
-
-/**
  * Implement hook_theme().
  */
 function field_theme() {
Index: modules/field/field.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.test,v
retrieving revision 1.44
diff -u -p -r1.44 field.test
--- modules/field/field.test	26 Aug 2009 02:59:22 -0000	1.44
+++ modules/field/field.test	26 Aug 2009 15:25:59 -0000
@@ -1375,8 +1375,7 @@ class FieldFormTestCase extends FieldTes
       $pattern[$weight] = "<input [^>]*value=\"$value\" [^>]*";
     }
     // Press 'add more' button through AHAH.
-    $path = 'field/js_add_more/' . str_replace('_', '-', $this->instance['bundle']) . '/' . str_replace('_', '-', $this->instance['field_name']);
-    $this->_fieldPostAhah($path, $edit, t('Add another item'));
+    $this->_fieldPostAhah($edit, t('Add another item'));
 
     ksort($values);
     $values = array_values($values);
@@ -1401,11 +1400,11 @@ class FieldFormTestCase extends FieldTes
    * Since the result is generally not a full-fledged form, this cannot be
    * called iteratively.
    */
-  function _fieldPostAhah($path, $edit, $submit, array $options = array(), array $headers = array()) {
+  function _fieldPostAhah($edit, $submit, array $options = array(), array $headers = array()) {
     // @TODO: the framework should make it possible to submit a form to a
     // different URL than its action or the current. For now, we can just force
     // it.
-    $this->additionalCurlOptions[CURLOPT_URL] = url($path, array('absolute' => TRUE));
+    $this->additionalCurlOptions[CURLOPT_URL] = url('system/ajax', array('absolute' => TRUE));
     $this->drupalPost(NULL, $edit, $submit);
     unset($this->additionalCurlOptions[CURLOPT_URL]);
 
