Integrate CTools AJAX framework with Drupal to extend (and replace) existing ahah framework

From: andrew morton <graveltech@gmail.com>

http://drupal.org/node/544418
---

 includes/ajax.inc             |  666 +++++++++++++++++++++++++++++++++++++++++
 includes/common.inc           |    1 
 includes/form.inc             |  135 --------
 misc/ahah.js                  |  259 ----------------
 misc/ajax.js                  |  393 ++++++++++++++++++++++++
 misc/autocomplete.js          |    2 
 misc/drupal.js                |    4 
 misc/progress.js              |    2 
 modules/book/book.css         |    2 
 modules/book/book.module      |    5 
 modules/book/book.pages.inc   |   42 +--
 modules/field/field.form.inc  |   21 -
 modules/field/field.test      |   15 +
 modules/poll/poll.module      |   10 -
 modules/poll/poll.test        |   19 +
 modules/system/system-rtl.css |    4 
 modules/system/system.css     |    8 
 modules/system/system.module  |   28 +-
 modules/upload/upload.module  |   16 +
 19 files changed, 1156 insertions(+), 476 deletions(-)
 create mode 100644 includes/ajax.inc
 delete mode 100644 misc/ahah.js
 create mode 100644 misc/ajax.js


diff --git includes/ajax.inc includes/ajax.inc
new file mode 100644
index 0000000..e2539ab
--- /dev/null
+++ includes/ajax.inc
@@ -0,0 +1,666 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Functions for use with Drupal's AJAX framework.
+ */
+
+/**
+ * @defgroup ajax AJAX framework
+ * @{
+ *
+ * Drupal's AJAX framework creates a PHP macro language that can be used to
+ * instruct JavaScript to perform actions on the client browser. When using
+ * forms, this framework can be used automatically with the #ajax property.
+ *
+ * To implement AJAX handling in a normal form, just add '#ajax' to the FAPI
+ * definition of a field. That field will trigger an AJAX event when it is
+ * clicked (or changed, depending on the kind of field). #ajax supports
+ * the following parameters:
+ *   - #ajax['path']: The path to use for the request. By default this is
+ *     system/ajax. Be warned that the default path currently only works
+ *     for buttons. It will not work for selects, textfields, or textareas.
+ *   - #ajax['callback']: If using the default path (system/ajax), this
+ *     callback will be triggered to handle the server side of the #ajax
+ *     event. The callback will receive $form and $form_state as arguments,
+ *     and should return the HTML to replace.
+ *   - #ajax['wrapper']: The ID of the AJAX area. The HTML returned from the
+ *     callback will replace whatever is currently in this wrapper. It is
+ *     important to ensure that this wrapper exists in the form, and is often
+ *     created using #prefix and #suffix tags.
+ *   - #ajax['effect']: The jQuery effect to use when placing the new HTML.
+ *     Defaults to no effect. Valid options are 'none', 'slide', or 'fade'.
+ *   - #ajax['speed']: The effect speed to use. Defaults to 'slow'. May be
+ *     'slow', 'fast' or a number in milliseconds which represents the length
+ *     of time the effect should run.
+ *   - #ajax['event']: The javascript event to respond to. This is normally
+ *     selected automatically for the type of form widget being used, and
+ *     is only needed if you need to override the default behavior.
+ *   - #ajax['method']: The jQuery method to use to place the new HTML.
+ *     Defaults to 'replace'. May be: 'replace', 'append', 'prepend',
+ *     'before', 'after', or 'html'. See the jQuery documentation for more
+ *     information on these methods.
+ *
+ * In addition to using FAPI for doing in-form modification, AJAX may be
+ * enabled by adding classes to buttons and links. By adding the 'use-ajax'
+ * class to a link, the link will be loaded via an AJAX call. This is
+ * particularly useful for things such as popups. When using this method,
+ * the href of the link can contain '/nojs/' as part of the path. When
+ * the AJAX framework makes the request, it will convert this to '/ajax/'.
+ * The server can then easily tell if this request was made through an
+ * actual AJAX request or in a degraded state, and respond appropriately.
+ *
+ * Similarly, submit buttons can be given the class 'use-ajax-submit'. The
+ * form will then be submitted via AJAX to the path specified in the #action.
+ * Like the ajax-submit class above, this path will have '/nojs/' replaced with
+ * '/ajax/' so that the submit handler can tell if the form was submitted
+ * in a degraded state or not.
+ *
+ * When responding to AJAX requests, the server should do what it needs to do
+ * for that request, then create a commands array. This commands array will
+ * be converted to a JSON object and returned to the client, which will then
+ * iterate over the array and process it like a macro language.
+ *
+ * Each command is an object. $object->command is the type of command and will
+ * be used to find the function (it will correllate directly to a function in
+ * the Drupal.AJAX.Command space). The object may contain any other data that
+ * the command needs to process.
+ *
+ * @see ajax_commands
+ *
+ * Commands are usually created with a couple of helper functions, so they
+ * look like this:
+ *
+ * @code
+ *   $commands = array();
+ *   $commands[] = ajax_command_replace('#object-1', 'some html here');
+ *   $commands[] = ajax_command_changed('#object-1');
+ *   ajax_render($commands); // Ends the request.
+ * @endcode
+ */
+
+/**
+ * Render a commands array into JSON and exit.
+ *
+ * Commands are immediately handed back to the AJAX requester. This function
+ * will render and immediately exit.
+ *
+ * @param $commands
+ *   A list of macro commands generated by the use of ajax_command_*()
+ *   functions.
+ * @param $header
+ *   If set to FALSE the 'text/javascript' header used by drupal_json() will
+ *   not be used, which is necessary when using an <iframe>. If set to
+ *   'multipart' the output will be wrapped in a <textarea>, which can also be
+ *   used as an alternative method when uploading files.
+ */
+function ajax_render($commands = array(), $header = TRUE) {
+  // Automatically extract any 'settings' added via drupal_add_js() and make
+  // them the first command.
+  $javascript = drupal_add_js(NULL, NULL);
+  if (!empty($javascript['settings'])) {
+    array_unshift($commands, ajax_command_settings($javascript['settings']['data']));
+  }
+
+  // Use === here so that bool TRUE doesn't match 'multipart'.
+  if ($header === 'multipart') {
+    // We don't use drupal_json() here because the header is not true. We're
+    // not really returning JSON, strictly-speaking, but rather JSON content
+    // wrapped in a <textarea> as per the "file uploads" example here:
+    // http://malsup.com/jquery/form/#code-samples
+    print '<textarea>' . drupal_to_js($commands) . '</textarea>';
+  }
+  else if ($header) {
+    drupal_json($commands);
+  }
+  else {
+    print drupal_to_js($commands);
+  }
+  exit;
+}
+
+/**
+ * Send an error response back via AJAX and immediately exit.
+ *
+ * This function can be used to quickly create a command array with an error
+ * string and send it, short-circuiting the error handling process.
+ *
+ * @param $error
+ *   A string to display in an alert.
+ */
+function ajax_render_error($error = '') {
+  $commands = array();
+  $commands[] = ajax_command_error($error);
+  ajax_render($commands);
+}
+
+/**
+ * Get a form submitted via #ajax during an AJAX callback.
+ *
+ * This will load a form from the form cache used during AJAX operations. It
+ * pulls the form info from $_POST.
+ *
+ * @return
+ *   An array containing the $form and $form_state. Use the list() function
+ *   to break these apart:
+ *   @code
+ *     list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
+ *   @endcode
+ */
+function ajax_get_form() {
+  $form_state = form_state_defaults();
+
+  $form_build_id = $_POST['form_build_id'];
+
+  // Get the form from the cache.
+  $form = form_get_cache($form_build_id, $form_state);
+  if (!$form) {
+    // If $form cannot be loaded from the cache, the form_build_id in $_POST
+    // must be invalid, which means that someone performed a POST request onto
+    // system/ajax without actually viewing the concerned form in the browser.
+    // This is likely a hacking attempt as it never happens under normal
+    // circumstances, so we just do nothing.
+    exit;
+  }
+
+  // Since some of the submit handlers are run, redirects need to be disabled.
+  $form['#redirect'] = FALSE;
+
+  // The form needs to be processed; prepare for that by setting a few internal
+  // variables.
+  $form_state['input'] = $_POST;
+  $form_state['args'] = $form['#args'];
+  $form_id = $form['#form_id'];
+
+  return array($form, $form_state, $form_id, $form_build_id);
+}
+
+/**
+ * Menu callback for AJAX callbacks through the #ajax['callback'] Form API property.
+ */
+function ajax_form_callback() {
+  list($form, $form_state, $form_id, $form_build_id) = ajax_get_form();
+
+  // Build, validate and if possible, submit the form.
+  drupal_process_form($form_id, $form, $form_state);
+
+  // This call recreates the form relying solely on the $form_state that
+  // drupal_process_form() set up.
+  $form = drupal_rebuild_form($form_id, $form_state, $form_build_id);
+
+  // Get the callback function from the clicked button.
+  $ajax = $form_state['clicked_button']['#ajax'];
+  $callback = $ajax['callback'];
+  if (drupal_function_exists($callback)) {
+    $html = $callback($form, $form_state);
+
+    // If the returned value is a string, assume it is HTML and create
+    // a command object to return automatically.
+    if (is_string($html)) {
+      $commands = array();
+      $commands[] = ajax_command_replace(NULL, $html);
+    }
+    else {
+      $commands = $html;
+    }
+
+    ajax_render($commands);
+  }
+
+  // Return a 'do nothing' command if there was no callback.
+  ajax_render(array());
+}
+
+/**
+ * Add AJAX information about a form element to the page to communicate with JavaScript.
+ *
+ * If #ajax['path'] is set on an element, this additional JavaScript is added
+ * to the page header to attach the AJAX behaviors. See ajax.js for more
+ * information.
+ *
+ * @param $element
+ *   An associative array containing the properties of the element.
+ *   Properties used:
+ *   - #ajax['event']
+ *   - #ajax['path']
+ *   - #ajax['wrapper']
+ *   - #ajax['parameters']
+ *   - #ajax['effect']
+ * @return
+ *   None. Additional code is added to the header of the page using
+ *   drupal_add_js().
+ */
+function ajax_process_form($element) {
+  $js_added = &drupal_static(__FUNCTION__, array());
+
+  // Add a reasonable default event handler if none was specified.
+  if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) {
+    switch ($element['#type']) {
+      case 'submit':
+      case 'button':
+      case 'image_button':
+        // Use the mousedown instead of the click event because form
+        // submission via pressing the enter key triggers a click event on
+        // submit inputs, inappropriately triggering AJAX behaviors.
+        $element['#ajax']['event'] = 'mousedown';
+        // Attach an additional event handler so that AJAX behaviors
+        // can be triggered still via keyboard input.
+        $element['#ajax']['keypress'] = TRUE;
+        break;
+
+      case 'password':
+      case 'textfield':
+      case 'textarea':
+        $element['#ajax']['event'] = 'blur';
+        break;
+
+      case 'radio':
+      case 'checkbox':
+      case 'select':
+        $element['#ajax']['event'] = 'change';
+        break;
+
+      default:
+        return $element;
+    }
+  }
+
+  // Adding the same JavaScript settings twice will cause a recursion error,
+  // we avoid the problem by checking if the JavaScript has already been added.
+  if (!isset($js_added[$element['#id']]) && (isset($element['#ajax']['callback']) || isset($element['#ajax']['path'])) && isset($element['#ajax']['event'])) {
+    drupal_add_library('system', 'form');
+    drupal_add_js('misc/ajax.js');
+
+    $ajax_binding = array(
+      'url'      => isset($element['#ajax']['callback']) ? url('system/ajax') : url($element['#ajax']['path']),
+      'event'    => $element['#ajax']['event'],
+      'keypress' => empty($element['#ajax']['keypress']) ? NULL : $element['#ajax']['keypress'],
+      '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'],
+      '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,
+    );
+
+    // Convert a simple #ajax['progress'] type string into an array.
+    if (is_string($ajax_binding['progress'])) {
+      $ajax_binding['progress'] = array('type' => $ajax_binding['progress']);
+    }
+    // Change progress path to a full URL.
+    if (isset($ajax_binding['progress']['path'])) {
+      $ajax_binding['progress']['url'] = url($ajax_binding['progress']['path']);
+    }
+    // Add progress.js if we're doing a bar display.
+    if ($ajax_binding['progress']['type'] == 'bar') {
+      drupal_add_js('misc/progress.js', array('cache' => FALSE));
+    }
+
+    drupal_add_js(array('ajax' => array($element['#id'] => $ajax_binding)), 'setting');
+
+    $js_added[$element['#id']] = TRUE;
+    $element['#cache'] = TRUE;
+  }
+  return $element;
+}
+
+/**
+ * @} End of "defgroup ajax".
+ */
+
+/**
+ * @defgroup ajax_commands AJAX framework commands
+ * @{
+ */
+
+/**
+ * Creates a Drupal AJAX 'alert' command.
+ *
+ * The 'alert' command instructs the client to display a JavaScript alert
+ * dialog box.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.alert()
+ * defined in misc/ajax.js.
+ *
+ * @param $error
+ *   The error message string to render to the user.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_error($error = '') {
+  return array(
+    'command' => 'alert',
+    'title' => t('Error'),
+    'text' => !empty($error) ? $error : t('An error occurred while handling the request: The server received invalid input.'),
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'insert/replace' command.
+ *
+ * The 'insert/replace' command instructs the client to use jQuery's empty()
+ * function to clear each element matched matched by the given selector, then
+ * to use jQuery's append() function to insert the given HTML.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @param $html
+ *   The data to use with the jQuery replace() function.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ * @see http://docs.jquery.com/Manipulation/empty
+ * @see http://docs.jquery.com/Manipulation/append#content
+ */
+function ajax_command_replace($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'replace',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'insert/html' command.
+ *
+ * The 'insert/html' command instructs the client to use jQuery's html()
+ * function to set the HTML content of each element matched by the given
+ * selector while leaving the outer tags intact.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @param $html
+ *   The data to use with the jQuery html() function.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ * @see http://docs.jquery.com/Attributes/html#val
+ */
+function ajax_command_html($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'html',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'insert/prepend' command.
+ *
+ * The 'insert/prepend' command instructs the client to use jQuery's prepend()
+ * function to prepend the given HTML content to the inside each element matched
+ * by the given selector.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @param $html
+ *   The data to use with the jQuery prepend() function.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ * @see http://docs.jquery.com/Manipulation/prepend#content
+ */
+function ajax_command_prepend($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'prepend',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'insert/append' command.
+ *
+ * The 'insert/append' command instructs the client to use jQuery's append()
+ * function to append the given HTML content to the inside each element matched
+ * by the given selector.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @param $html
+ *   The data to use with the jQuery append() function.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ * @see http://docs.jquery.com/Manipulation/append#content
+ */
+function ajax_command_append($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'append',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'insert/after' command.
+ *
+ * The 'insert/after' command instructs the client to use jQuery's after()
+ * function to insert the given HTML content after each element matched by
+ * the given selector.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @param $html
+ *   The data to use with the jQuery after() function.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ * @see http://docs.jquery.com/Manipulation/after#content
+ */
+function ajax_command_after($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'after',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'insert/before' command.
+ *
+ * The 'insert/before' command instructs the client to use jQuery's before()
+ * function to insert the given HTML content before each of elements matched by
+ * the given selector.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @param $html
+ *   The data to use with the jQuery before() function.
+ * @param $settings
+ *   An optional array of settings that will be used for this command only.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ * @see http://docs.jquery.com/Manipulation/before#content
+ */
+function ajax_command_before($selector, $html, $settings = NULL) {
+  return array(
+    'command' => 'insert',
+    'method' => 'before',
+    'selector' => $selector,
+    'data' => $html,
+    'settings' => $settings,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'remove' command.
+ *
+ * The 'remove' command instructs the client to use jQuery's remove() function
+ * to remove each of elements matched by the given selector, and everything
+ * within them.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.remove()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ * @see http://docs.jquery.com/Manipulation/remove#expr
+ */
+function ajax_command_remove($selector) {
+  return array(
+    'command' => 'remove',
+    'selector' => $selector,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'changed' command.
+ *
+ * The 'changed' command instructs the client to mark each of the elements
+ * matched by the given selector as 'changed'.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.changed()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @param $star
+ *   An optional CSS selector which must be inside $selector. If specified,
+ *   a star will be appended to the HTML inside the $star selector.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_changed($selector, $star = '') {
+  return array(
+    'command' => 'changed',
+    'selector' => $selector,
+    'star' => $star,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'css' command.
+ *
+ * The 'css' command will instruct the client to use the jQuery css() function
+ * to apply the CSS arguments to elements matched by the given selector.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.insert()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @param $argument
+ *   An array of key/value pairs to set in the CSS for the selector.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ * @see http://docs.jquery.com/CSS/css#properties
+ */
+function ajax_command_css($selector, $argument) {
+  return array(
+    'command' => 'css',
+    'selector' => $selector,
+    'argument' => $argument,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'settings' command.
+ *
+ * The 'settings' command instructs the client to extend Drupal.settings with
+ * the given array.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.settings()
+ * defined in misc/ajax.js.
+ *
+ * @param $argument
+ *   An array of key/value pairs to add to the settings. This will be
+ *   utilized for all commands after this if they do not include their
+ *   own settings array.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_settings($argument) {
+  return array(
+    'command' => 'settings',
+    'argument' => $argument,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'data' command.
+ *
+ * The 'data' command instructs the client to attach the name=value pair of
+ * data to the selector via jQuery's data cache.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.data()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @param $name
+ *   The name or key (in the key value pair) of the data attached to this
+ *   selector.
+ * @param $value
+ *  The value of the data. Not just limited to strings can be any format.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ * @see http://docs.jquery.com/Core/data#namevalue
+ */
+function ajax_command_data($selector, $name, $value) {
+  return array(
+    'command' => 'data',
+    'selector' => $selector,
+    'name' => $name,
+    'value' => $value,
+  );
+}
+
+/**
+ * Creates a Drupal AJAX 'restripe' command.
+ *
+ * The 'restripe' command instructs the client to restripe a table. This is
+ * usually used after a table has been modifed by a replace or append command.
+ *
+ * This command is implemented by Drupal.ajax.prototype.commands.restripe()
+ * defined in misc/ajax.js.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any jQuery selector.
+ * @return
+ *   An array suitable for use with the ajax_render() function.
+ */
+function ajax_command_restripe($selector) {
+  return array(
+    'command' => 'restripe',
+    'selector' => $selector,
+  );
+}
+
diff --git includes/common.inc includes/common.inc
index 6778760..7191186 100644
--- includes/common.inc
+++ includes/common.inc
@@ -3538,6 +3538,7 @@ function _drupal_bootstrap_full() {
   require_once DRUPAL_ROOT . '/includes/form.inc';
   require_once DRUPAL_ROOT . '/includes/mail.inc';
   require_once DRUPAL_ROOT . '/includes/actions.inc';
+  require_once DRUPAL_ROOT . '/includes/ajax.inc';
   // Set the Drupal custom error handler.
   set_error_handler('_drupal_error_handler');
   set_exception_handler('_drupal_exception_handler');
diff --git includes/form.inc includes/form.inc
index db9af87..c3394d9 100644
--- includes/form.inc
+++ includes/form.inc
@@ -1801,48 +1801,6 @@ function weight_value(&$form) {
 }
 
 /**
- * Menu callback for AHAH callbacks through the #ahah['callback'] FAPI property.
- */
-function form_ahah_callback() {
-  $form_state = form_state_defaults();
-
-  $form_build_id = $_POST['form_build_id'];
-
-  // Get the form from the cache.
-  $form = form_get_cache($form_build_id, $form_state);
-  if (!$form) {
-    // If $form cannot be loaded from the cache, the form_build_id in $_POST must
-    // be invalid, which means that someone performed a POST request onto
-    // system/ahah without actually viewing the concerned form in the browser.
-    // This is likely a hacking attempt as it never happens under normal
-    // circumstances, so we just do nothing.
-    exit;
-  }
-
-  // We will run some of the submit handlers so we need to disable redirecting.
-  $form['#redirect'] = FALSE;
-
-  // We need to process the form, prepare for that by setting a few internals
-  // variables.
-  $form_state['input'] = $_POST;
-  $form_state['args'] = $form['#args'];
-  $form_id = $form['#form_id'];
-
-  // Build, validate and if possible, submit the form.
-  drupal_process_form($form_id, $form, $form_state);
-
-  // This call recreates the form relying solely on the form_state that the
-  // drupal_process_form set up.
-  $form = drupal_rebuild_form($form_id, $form_state, $form_build_id);
-
-  // Get the callback function from the clicked button.
-  $callback = $form_state['clicked_button']['#ahah']['callback'];
-  if (drupal_function_exists($callback)) {
-    $callback($form, $form_state);
-  }
-}
-
-/**
  * Roll out a single radios element to a list of radios,
  * using the options array as index.
  */
@@ -1861,7 +1819,7 @@ function form_process_radios($element) {
           '#attributes' => $element['#attributes'],
           '#parents' => $element['#parents'],
           '#id' => form_clean_id('edit-' . implode('-', $parents_for_id)),
-          '#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL,
+          '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
         );
       }
     }
@@ -1971,91 +1929,6 @@ function theme_text_format_wrapper($element) {
 }
 
 /**
- * Add AHAH information about a form element to the page to communicate with
- * javascript. If #ahah[path] is set on an element, this additional javascript is
- * added to the page header to attach the AHAH behaviors. See ahah.js for more
- * information.
- *
- * @param $element
- *   An associative array containing the properties of the element.
- *   Properties used: ahah_event, ahah_path, ahah_wrapper, ahah_parameters,
- *   ahah_effect.
- * @return
- *   None. Additional code is added to the header of the page using
- *   drupal_add_js.
- */
-function form_process_ahah($element) {
-  $js_added = &drupal_static(__FUNCTION__, array());
-  // Add a reasonable default event handler if none specified.
-  if (isset($element['#ahah']) && !isset($element['#ahah']['event'])) {
-    switch ($element['#type']) {
-      case 'submit':
-      case 'button':
-      case 'image_button':
-        // Use the mousedown instead of the click event because form
-        // submission via pressing the enter key triggers a click event on
-        // submit inputs, inappropriately triggering AHAH behaviors.
-        $element['#ahah']['event'] = 'mousedown';
-        // Attach an additional event handler so that AHAH behaviors
-        // can be triggered still via keyboard input.
-        $element['#ahah']['keypress'] = TRUE;
-        break;
-      case 'password':
-      case 'textfield':
-      case 'textarea':
-        $element['#ahah']['event'] = 'blur';
-        break;
-      case 'radio':
-      case 'checkbox':
-      case 'select':
-        $element['#ahah']['event'] = 'change';
-        break;
-      default:
-        return $element;
-    }
-  }
-
-  // Adding the same javascript settings twice will cause a recursion error,
-  // we avoid the problem by checking if the javascript has already been added.
-  if ((isset($element['#ahah']['callback']) || isset($element['#ahah']['path'])) && isset($element['#ahah']['event']) && !isset($js_added[$element['#id']])) {
-    drupal_add_library('system', 'form');
-    drupal_add_js('misc/ahah.js');
-
-    $ahah_binding = array(
-      'url'      => isset($element['#ahah']['callback']) ? url('system/ahah') : url($element['#ahah']['path']),
-      'event'    => $element['#ahah']['event'],
-      'keypress' => empty($element['#ahah']['keypress']) ? NULL : $element['#ahah']['keypress'],
-      'wrapper'  => empty($element['#ahah']['wrapper']) ? NULL : $element['#ahah']['wrapper'],
-      'selector' => empty($element['#ahah']['selector']) ? '#' . $element['#id'] : $element['#ahah']['selector'],
-      'effect'   => empty($element['#ahah']['effect']) ? 'none' : $element['#ahah']['effect'],
-      'method'   => empty($element['#ahah']['method']) ? 'replace' : $element['#ahah']['method'],
-      'progress' => empty($element['#ahah']['progress']) ? array('type' => 'throbber') : $element['#ahah']['progress'],
-      'button'   => isset($element['#executes_submit_callback']) ? array($element['#name'] => $element['#value']) : FALSE,
-    );
-
-    // Convert a simple #ahah[progress] type string into an array.
-    if (is_string($ahah_binding['progress'])) {
-      $ahah_binding['progress'] = array('type' => $ahah_binding['progress']);
-    }
-    // Change progress path to a full url.
-    if (isset($ahah_binding['progress']['path'])) {
-      $ahah_binding['progress']['url'] = url($ahah_binding['progress']['path']);
-    }
-
-    // Add progress.js if we're doing a bar display.
-    if ($ahah_binding['progress']['type'] == 'bar') {
-      drupal_add_js('misc/progress.js', array('cache' => FALSE));
-    }
-
-    drupal_add_js(array('ahah' => array($element['#id'] => $ahah_binding)), 'setting');
-
-    $js_added[$element['#id']] = TRUE;
-    $element['#cache'] = TRUE;
-  }
-  return $element;
-}
-
-/**
  * Format a checkbox.
  *
  * @param $element
@@ -2132,7 +2005,7 @@ function form_process_checkboxes($element) {
           '#return_value' => $key,
           '#default_value' => isset($value[$key]),
           '#attributes' => $element['#attributes'],
-          '#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL,
+          '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
         );
       }
     }
@@ -2245,7 +2118,7 @@ function form_process_tableselect($element) {
             '#return_value' => $key,
             '#default_value' => isset($value[$key]),
             '#attributes' => $element['#attributes'],
-            '#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL,
+            '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
           );
         }
         else {
@@ -2260,7 +2133,7 @@ function form_process_tableselect($element) {
             '#attributes' => $element['#attributes'],
             '#parents' => $element['#parents'],
             '#id' => form_clean_id('edit-' . implode('-', $parents_for_id)),
-            '#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL,
+            '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
           );
         }
       }
diff --git misc/ahah.js misc/ahah.js
deleted file mode 100644
index b9ec665..0000000
--- misc/ahah.js
+++ /dev/null
@@ -1,259 +0,0 @@
-// $Id: ahah.js,v 1.17 2009-07-16 02:46:13 dries Exp $
-(function ($) {
-
-/**
- * Provides AJAX-like page updating via AHAH (Asynchronous HTML and HTTP).
- *
- * AHAH is a method of making a request via Javascript while viewing an HTML
- * page. The request returns a small chunk of HTML, which is then directly
- * injected into the page.
- *
- * Drupal uses this file to enhance form elements with #ahah[path] and
- * #ahah[wrapper] properties. If set, this file will automatically be included
- * to provide AHAH capabilities.
- */
-
-Drupal.ahah = Drupal.ahah || {};
-
-/**
- * Attaches the ahah behavior to each ahah form element.
- */
-Drupal.behaviors.ahah = {
-  attach: function (context, settings) {
-    for (var base in settings.ahah) {
-      if (!$('#' + base + '.ahah-processed').size()) {
-        var element_settings = settings.ahah[base];
-
-        $(element_settings.selector).each(function () {
-          element_settings.element = this;
-          Drupal.ahah[base] = new Drupal.ahah(base, element_settings);
-        });
-
-        $('#' + base).addClass('ahah-processed');
-      }
-    }
-  }
-};
-
-/**
- * AHAH object.
- *
- * All AHAH objects on a page are accessible through the global Drupal.ahah object
- * and are keyed by the submit button's ID. You can access them from your module's
- * JavaScript file to override properties or functions.
- * For example, if your AHAH enabled button has the ID 'edit-submit', you can
- * redefine the function that is called to insert the new content like this
- * (inside a Drupal.behaviors attach block):
- * @code
- *    Drupal.behaviors.myCustomAhahStuff = {
- *      attach: function(context, settings) {
- *        Drupal.ahah['edit-submit'].insertNewContent = function(response, status) {
- *          new_content = $(response.data);
- *          $('#my-wrapper').append(new_content);
- *          alert('New content was appended to #my-wrapper');
- *        }
- *      }
- *    };
- * @endcode
- */
-Drupal.ahah = function (base, element_settings) {
-  // Set the properties for this object.
-  this.element = element_settings.element;
-  this.selector = element_settings.selector;
-  this.event = element_settings.event;
-  this.keypress = element_settings.keypress;
-  this.url = element_settings.url;
-  this.wrapper = '#' + element_settings.wrapper;
-  this.effect = element_settings.effect;
-  this.method = element_settings.method;
-  this.progress = element_settings.progress;
-  this.button = element_settings.button || { };
-
-  if (this.effect == 'none') {
-    this.showEffect = 'show';
-    this.hideEffect = 'hide';
-    this.showSpeed = '';
-  }
-  else if (this.effect == 'fade') {
-    this.showEffect = 'fadeIn';
-    this.hideEffect = 'fadeOut';
-    this.showSpeed = 'slow';
-  }
-  else {
-    this.showEffect = this.effect + 'Toggle';
-    this.hideEffect = this.effect + 'Toggle';
-    this.showSpeed = 'slow';
-  }
-
-  // Record the form action and target, needed for iFrame file uploads.
-  var form = $(this.element).parents('form');
-  this.form_action = form.attr('action');
-  this.form_target = form.attr('target');
-  this.form_encattr = form.attr('encattr');
-
-  // Set the options for the ajaxSubmit function.
-  // The 'this' variable will not persist inside of the options object.
-  var ahah = this;
-  var options = {
-    url: ahah.url,
-    data: ahah.button,
-    beforeSubmit: function (form_values, element_settings, options) {
-      return ahah.beforeSubmit(form_values, element_settings, options);
-    },
-    success: function (response, status) {
-      // Sanity check for browser support (object expected).
-      // When using iFrame uploads, responses must be returned as a string.
-      if (typeof response == 'string') {
-        response = Drupal.parseJson(response);
-      }
-      return ahah.success(response, status);
-    },
-    complete: function (response, status) {
-      if (status == 'error' || status == 'parsererror') {
-        return ahah.error(response, ahah.url);
-      }
-    },
-    dataType: 'json',
-    type: 'POST'
-  };
-
-  // Bind the ajaxSubmit function to the element event.
-  $(element_settings.element).bind(element_settings.event, function () {
-    $(element_settings.element).parents('form').ajaxSubmit(options);
-    return false;
-  });
-  // If necessary, enable keyboard submission so that AHAH behaviors
-  // can be triggered through keyboard input as well as e.g. a mousedown
-  // action.
-  if (element_settings.keypress) {
-    $(element_settings.element).keypress(function (event) {
-      // Detect enter key.
-      if (event.keyCode == 13) {
-        $(element_settings.element).trigger(element_settings.event);
-        return false;
-      }
-    });
-  }
-};
-
-/**
- * Handler for the form redirection submission.
- */
-Drupal.ahah.prototype.beforeSubmit = function (form_values, element, options) {
-  // Disable the element that received the change.
-  $(this.element).addClass('progress-disabled').attr('disabled', true);
-
-  // Insert progressbar or throbber.
-  if (this.progress.type == 'bar') {
-    var progressBar = new Drupal.progressBar('ahah-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback));
-    if (this.progress.message) {
-      progressBar.setProgress(-1, this.progress.message);
-    }
-    if (this.progress.url) {
-      progressBar.startMonitoring(this.progress.url, this.progress.interval || 1500);
-    }
-    this.progress.element = $(progressBar.element).addClass('ahah-progress ahah-progress-bar');
-    this.progress.object = progressBar;
-    $(this.element).after(this.progress.element);
-  }
-  else if (this.progress.type == 'throbber') {
-    this.progress.element = $('<div class="ahah-progress ahah-progress-throbber"><div class="throbber">&nbsp;</div></div>');
-    if (this.progress.message) {
-      $('.throbber', this.progress.element).after('<div class="message">' + this.progress.message + '</div>');
-    }
-    $(this.element).after(this.progress.element);
-  }
-};
-
-/**
- * Handler for the form redirection completion.
- */
-Drupal.ahah.prototype.success = function (response, status) {
-  var form = $(this.element).parents('form');
-
-  // Restore the previous action and target to the form.
-  form.attr('action', this.form_action);
-  this.form_target ? form.attr('target', this.form_target) : form.removeAttr('target');
-  this.form_encattr ? form.attr('target', this.form_encattr) : form.removeAttr('encattr');
-
-  // Remove the progress element.
-  if (this.progress.element) {
-    $(this.progress.element).remove();
-  }
-  if (this.progress.object) {
-    this.progress.object.stopMonitoring();
-  }
-  $(this.element).removeClass('progress-disabled').attr('disabled', false);
-
-  Drupal.freezeHeight();
-
-  // Call the insertNewContent handler to insert the new content into the page.
-  this.insertNewContent(response, status);
-
-  Drupal.unfreezeHeight();
-};
-
-/**
- * Handler to insert the new content into the page.
- */
-Drupal.ahah.prototype.insertNewContent = function (response, status) {
-  var wrapper = $(this.wrapper);
-
-  // Manually insert HTML into the jQuery object, using $() directly crashes
-  // Safari with long string lengths. http://dev.jquery.com/ticket/1152
-  var new_content = $('<div></div>').html(response.data);
-
-  // Add the new content to the page.
-  if (this.method == 'replace') {
-    wrapper.empty().append(new_content);
-  }
-  else {
-    wrapper[this.method](new_content);
-  }
-
-  // Immediately hide the new content if we're using any effects.
-  if (this.showEffect != 'show') {
-    new_content.hide();
-  }
-
-  // Determine what effect use and what content will receive the effect, then
-  // show the new content.
-  if ($('.ahah-new-content', new_content).size() > 0) {
-    $('.ahah-new-content', new_content).hide();
-    new_content.show();
-    $('.ahah-new-content', new_content)[this.showEffect](this.showSpeed);
-  }
-  else if (this.showEffect != 'show') {
-    new_content[this.showEffect](this.showSpeed);
-  }
-
-  // Attach all javascript behaviors to the new content, if it was successfully
-  // added to the page, this if statement allows #ahah[wrapper] to be optional.
-  if (new_content.parents('html').length > 0) {
-    // Apply any settings from the returned JSON if available.
-    var settings = response.settings || Drupal.settings;
-    Drupal.attachBehaviors(new_content, settings);
-  }
-};
-
-/**
- * Handler for the form redirection error.
- */
-Drupal.ahah.prototype.error = function (response, uri) {
-  alert(Drupal.ahahError(response, uri));
-  // Resore the previous action and target to the form.
-  $(this.element).parent('form').attr({ action: this.form_action, target: this.form_target });
-  // Remove the progress element.
-  if (this.progress.element) {
-    $(this.progress.element).remove();
-  }
-  if (this.progress.object) {
-    this.progress.object.stopMonitoring();
-  }
-  // Undo hide.
-  $(this.wrapper).show();
-  // Re-enable the element.
-  $(this.element).removeClass('progress-disabled').attr('disabled', false);
-};
-
-})(jQuery);
diff --git misc/ajax.js misc/ajax.js
new file mode 100644
index 0000000..2e61031
--- /dev/null
+++ misc/ajax.js
@@ -0,0 +1,393 @@
+// $Id$
+(function ($) {
+
+/**
+ * Provides AJAX page updating via jQuery $.ajax (Asynchronous JavaScript and XML).
+ *
+ * AJAX is a method of making a request via Javascript while viewing an HTML
+ * page. The request returns an array of commands encoded in JSON, which is
+ * then executed to make any changes that are necessary to the page.
+ *
+ * Drupal uses this file to enhance form elements with #ajax['path'] and
+ * #ajax['wrapper'] properties. If set, this file will automatically be included
+ * to provide AJAX capabilities.
+ */
+
+Drupal.ajax = Drupal.ajax || {};
+
+/**
+ * Attaches the AJAX behavior to each AJAX form element.
+ */
+Drupal.behaviors.AJAX = {
+  attach: function (context, settings) {
+    // Load all AJAX behaviors specified in the settings.
+    for (var base in settings.ajax) {
+      if (!$('#' + base + '.ajax-processed').size()) {
+        var element_settings = settings.ajax[base];
+
+        $(element_settings.selector).each(function () {
+          Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
+        });
+
+        $('#' + base).addClass('ajax-processed');
+      }
+    }
+
+    // Bind AJAX behaviors to all items showing the class.
+    $('.use-ajax:not(.ajax-processed)')
+      .addClass('ajax-processed')
+      .each(function () {
+      var element_settings = {};
+
+      // For anchor tags, these will go to the target of the anchor rather
+      // than the usual location.
+      if ($(this).attr('href')) {
+        element_settings.url = $(this).attr('href');
+      }
+      var base = $(this).attr('id');
+      Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
+    });
+
+    // This class means to submit the form to the action using ajax.
+    $('.use-ajax-submit:not(.ajax-processed)')
+      .addClass('ajax-processed')
+      .each(function () {
+      var element_settings = {};
+
+      // AJAX submits specified in this manner automatically submit to the
+      // normal form action.
+      element_settings.url = $(this.form).attr('action');
+      element_settings.set_click = TRUE;
+
+      var base = $(this).attr('id');
+      Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
+    });
+  }
+};
+
+/**
+ * AJAX object.
+ *
+ * All AJAX objects on a page are accessible through the global Drupal.ajax
+ * object and are keyed by the submit button's ID. You can access them from
+ * your module's JavaScript file to override properties or functions.
+ *
+ * For example, if your AJAX enabled button has the ID 'edit-submit', you can
+ * redefine the function that is called to insert the new content like this
+ * (inside a Drupal.behaviors attach block):
+ * @code
+ *    Drupal.behaviors.myCustomAJAXStuff = {
+ *      attach: function (context, settings) {
+ *        Drupal.ajax['edit-submit'].commands.insert = function (ajax, response, status) {
+ *          new_content = $(response.data);
+ *          $('#my-wrapper').append(new_content);
+ *          alert('New content was appended to #my-wrapper');
+ *        }
+ *      }
+ *    };
+ * @endcode
+ */
+Drupal.ajax = function (base, element, element_settings) {
+  var defaults = {
+    url: 'system/ajax',
+    event: 'mousedown',
+    keypress: true,
+    selector: '#' + base,
+    effect: 'none',
+    speed: 'slow',
+    method: 'replace',
+    progress: {
+      type: 'bar',
+      message: 'Please wait...'
+    },
+    button: {},
+  };
+
+  $.extend(this, defaults, element_settings);
+
+  this.element = element;
+
+  // Replacing 'nojs' with 'ajax' in the URL allows for an easy method to
+  // let the server detect when it needs to degrade gracefully.
+  this.url = element_settings.url.replace('/nojs/', '/ajax/');
+  this.wrapper = '#' + element_settings.wrapper;
+
+  // If there isn't a form, jQuery.ajax() will be used instead, allowing us to
+  // bind AJAX to links as well.
+  if (this.element.form) {
+    this.form = $(this.element.form);
+  }
+
+  // Set the options for the ajaxSubmit function.
+  // The 'this' variable will not persist inside of the options object.
+  var ajax = this;
+  var options = {
+    url: ajax.url,
+    data: ajax.button,
+    beforeSubmit: function (form_values, element_settings, options) {
+      return ajax.beforeSubmit(form_values, element_settings, options);
+    },
+    success: function (response, status) {
+      // Sanity check for browser support (object expected).
+      // When using iFrame uploads, responses must be returned as a string.
+      if (typeof response == 'string') {
+        response = Drupal.parseJson(response);
+      }
+      return ajax.success(response, status);
+    },
+    complete: function (response, status) {
+      if (status == 'error' || status == 'parsererror') {
+        return ajax.error(response, ajax.url);
+      }
+    },
+    dataType: 'json',
+    type: 'POST'
+  };
+
+  // Bind the ajaxSubmit function to the element event.
+  $(this.element).bind(element_settings.event, function () {
+    if (ajax.form) {
+      // If setClick is set, we must set this to ensure that the button's
+      // value is passed.
+      if (ajax.setClick) {
+        // Mark the clicked button. 'form.clk' is a special variable for
+        // ajaxSubmit that tells the system which element got clicked to
+        // trigger the submit. Without it there would be no 'op' or
+        // equivalent.
+        ajax.form.clk = this.element;
+      }
+
+      ajax.form.ajaxSubmit(options);
+    }
+    else {
+      jQuery.ajax(options);
+    }
+
+    return false;
+  });
+
+  // If necessary, enable keyboard submission so that AJAX behaviors
+  // can be triggered through keyboard input as well as e.g. a mousedown
+  // action.
+  if (element_settings.keypress) {
+    $(element_settings.element).keypress(function (event) {
+      // Detect enter key.
+      if (event.keyCode == 13) {
+        $(element_settings.element).trigger(element_settings.event);
+        return false;
+      }
+    });
+  }
+};
+
+/**
+ * Handler for the form redirection submission.
+ */
+Drupal.ajax.prototype.beforeSubmit = function (form_values, element, options) {
+  // Disable the element that received the change.
+  $(this.element).addClass('progress-disabled').attr('disabled', true);
+
+  // Insert progressbar or throbber.
+  if (this.progress.type == 'bar') {
+    var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback));
+    if (this.progress.message) {
+      progressBar.setProgress(-1, this.progress.message);
+    }
+    if (this.progress.url) {
+      progressBar.startMonitoring(this.progress.url, this.progress.interval || 1500);
+    }
+    this.progress.element = $(progressBar.element).addClass('ajax-progress ajax-progress-bar');
+    this.progress.object = progressBar;
+    $(this.element).after(this.progress.element);
+  }
+  else if (this.progress.type == 'throbber') {
+    this.progress.element = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
+    if (this.progress.message) {
+      $('.throbber', this.progress.element).after('<div class="message">' + this.progress.message + '</div>');
+    }
+    $(this.element).after(this.progress.element);
+  }
+};
+
+/**
+ * Handler for the form redirection completion.
+ */
+Drupal.ajax.prototype.success = function (response, status) {
+  // Remove the progress element.
+  if (this.progress.element) {
+    $(this.progress.element).remove();
+  }
+  if (this.progress.object) {
+    this.progress.object.stopMonitoring();
+  }
+  $(this.element).removeClass('progress-disabled').attr('disabled', false);
+
+  Drupal.freezeHeight();
+
+  for (i in response) {
+    if (response[i]['command'] && this.commands[response[i]['command']]) {
+      this.commands[response[i]['command']](this, response[i], status);
+    }
+  }
+
+  Drupal.unfreezeHeight();
+
+  // Remove any response-specific settings so they don't get used on the next
+  // call by mistake.
+  this.settings = {};
+};
+
+/**
+ * Build an effect object which tells us how to apply the effect when adding new HTML.
+ */
+Drupal.ajax.prototype.getEffect = function (response) {
+  var type = response.effect || this.effect;
+  var speed = response.speed || this.speed;
+
+  var effect = {};
+  if (type == 'none') {
+    effect.showEffect = 'show';
+    effect.hideEffect = 'hide';
+    effect.showSpeed = '';
+  }
+  else if (type == 'fade') {
+    effect.showEffect = 'fadeIn';
+    effect.hideEffect = 'fadeOut';
+    effect.showSpeed = speed;
+  }
+  else {
+    effect.showEffect = type + 'Toggle';
+    effect.hideEffect = type + 'Toggle';
+    effect.showSpeed = speed;
+  }
+
+  return effect;
+}
+
+/**
+ * Handler for the form redirection error.
+ */
+Drupal.ajax.prototype.error = function (response, uri) {
+  alert(Drupal.ajaxError(response, uri));
+  // Remove the progress element.
+  if (this.progress.element) {
+    $(this.progress.element).remove();
+  }
+  if (this.progress.object) {
+    this.progress.object.stopMonitoring();
+  }
+  // Undo hide.
+  $(this.wrapper).show();
+  // Re-enable the element.
+  $(this.element).removeClass('progress-disabled').attr('disabled', false);
+};
+
+/**
+ * Provide a series of commands that the server can request the client perform.
+ */
+Drupal.ajax.prototype.commands = {
+  /**
+   * Command to insert new content into the DOM.
+   */
+  insert: function (ajax, response, status) {
+    // Get information from the response. If it is not there, default to
+    // our presets.
+    var wrapper = response.selector ? $(response.selector) : $(ajax.wrapper);
+    var method = response.method || ajax.method;
+    var effect = ajax.getEffect(response);
+
+    // Manually insert HTML into the jQuery object, using $() directly crashes
+    // Safari with long string lengths. http://dev.jquery.com/ticket/3178
+    var new_content = $('<div></div>').html(response.data);
+
+    // Add the new content to the page.
+    if (method == 'replace') {
+      wrapper.empty().append(new_content);
+    }
+    else {
+      wrapper[method](new_content);
+    }
+
+    // Immediately hide the new content if we're using any effects.
+    if (effect.showEffect != 'show') {
+      new_content.hide();
+    }
+
+    // Determine which effect to use and what content will receive the
+    // effect, then show the new content.
+    if ($('.ajax-new-content', new_content).size() > 0) {
+      $('.ajax-new-content', new_content).hide();
+      new_content.show();
+      $('.ajax-new-content', new_content)[effect.showEffect](effect.showSpeed);
+    }
+    else if (effect.showEffect != 'show') {
+      new_content[effect.showEffect](effect.showSpeed);
+    }
+
+    // Attach all JavaScript behaviors to the new content, if it was successfully
+    // added to the page, this if statement allows #ajax[wrapper] to be optional.
+    if (new_content.parents('html').length > 0) {
+      // Apply any settings from the returned JSON if available.
+      var settings = response.settings || ajax.settings || Drupal.settings;
+      Drupal.attachBehaviors(new_content, settings);
+    }
+  },
+
+  /**
+   * Command to remove a chunk from the page.
+   */
+  remove: function (ajax, response, status) {
+    $(response.selector).remove();
+  },
+
+  /**
+   * Command to mark a chunk changed.
+   */
+  changed: function (ajax, response, status) {
+    if (!$(response.selector).hasClass('changed')) {
+      $(response.selector).addClass('changed');
+      if (response.star) {
+        $(response.selector).find(response.star).append(' <span class="star">*</span> ');
+      }
+    }
+  },
+
+  /**
+   * Command to provide an alert.
+   */
+  alert: function (ajax, response, status) {
+    alert(response.text, response.title);
+  },
+
+  /**
+   * Command to set the settings that will be used for other commands in this response.
+   */
+  settings: function (ajax, response, status) {
+    ajax.settings = response.settings;
+  },
+
+  /**
+   * Command to attach data using jQuery's data API.
+   */
+  data: function (ajax, response, status) {
+    $(response.selector).data(response.name, response.value);
+  },
+
+  /**
+   * Command to restripe a table.
+   */
+  restripe: function (ajax, response, status) {
+    // :even and :odd are reversed because jQuery counts from 0 and
+    // we count from 1, so we're out of sync.
+    $('tbody tr:not(:hidden)', $(response.selector))
+      .removeClass('even')
+      .removeClass('odd')
+      .filter(':even')
+        .addClass('odd')
+      .end()
+      .filter(':odd')
+        .addClass('even');
+  }
+};
+
+})(jQuery);
diff --git misc/autocomplete.js misc/autocomplete.js
index 60e9f4d..27c89d1 100644
--- misc/autocomplete.js
+++ misc/autocomplete.js
@@ -289,7 +289,7 @@ Drupal.ACDB.prototype.search = function (searchString) {
         }
       },
       error: function (xmlhttp) {
-        alert(Drupal.ahahError(xmlhttp, db.uri));
+        alert(Drupal.ajaxError(xmlhttp, db.uri));
       }
     });
   }, this.delay);
diff --git misc/drupal.js misc/drupal.js
index 8273cd0..273d49d 100644
--- misc/drupal.js
+++ misc/drupal.js
@@ -294,9 +294,9 @@ Drupal.getSelection = function (element) {
 };
 
 /**
- * Build an error message from ahah response.
+ * Build an error message from an AJAX response.
  */
-Drupal.ahahError = function (xmlhttp, uri) {
+Drupal.ajaxError = function (xmlhttp, uri) {
   if (xmlhttp.status == 200 || (xmlhttp.status == 500 && xmlhttp.statusText == 'Service unavailable (with message)')) {
     if ($.trim(xmlhttp.responseText)) {
       var message = Drupal.t("An error occurred. \nPath: @uri\nMessage: !text", { '@uri': uri, '!text': xmlhttp.responseText });
diff --git misc/progress.js misc/progress.js
index c50fa82..cf63c59 100644
--- misc/progress.js
+++ misc/progress.js
@@ -84,7 +84,7 @@ Drupal.progressBar.prototype.sendPing = function () {
         pb.timer = setTimeout(function () { pb.sendPing(); }, pb.delay);
       },
       error: function (xmlhttp) {
-        pb.displayError(Drupal.ahahError(xmlhttp, pb.uri));
+        pb.displayError(Drupal.ajaxError(xmlhttp, pb.uri));
       }
     });
   }
diff --git modules/book/book.css modules/book/book.css
index 4afce86..e36c9d3 100644
--- modules/book/book.css
+++ modules/book/book.css
@@ -47,7 +47,7 @@ html.js #edit-book-pick-book {
 #book-admin-edit select.progress-disabled {
   margin-right: 0;
 }
-#book-admin-edit tr.ahah-new-content {
+#book-admin-edit tr.ajax-new-content {
   background-color: #ffd;
 }
 #book-admin-edit .form-item {
diff --git modules/book/book.module modules/book/book.module
index 01c7d04..90c46e6 100644
--- modules/book/book.module
+++ modules/book/book.module
@@ -490,10 +490,11 @@ function _book_add_form_elements(&$form, $node) {
     '#description' => t('Your page will be a part of the selected book.'),
     '#weight' => -5,
     '#attributes' => array('class' => 'book-title-select'),
-    '#ahah' => array(
+    '#ajax' => array(
       'path' => 'book/js/form',
       'wrapper' => 'edit-book-plid-wrapper',
-      'effect' => 'slide',
+      'effect' => 'fade',
+      'speed' => 'fast',
     ),
   );
 }
diff --git modules/book/book.pages.inc modules/book/book.pages.inc
index b883586..d0402a7 100644
--- modules/book/book.pages.inc
+++ modules/book/book.pages.inc
@@ -234,28 +234,26 @@ function book_remove_form_submit($form, &$form_state) {
  *   Prints the replacement HTML in JSON format.
  */
 function book_form_update() {
-  $cached_form_state = array();
+  // Load the form based upon the $_POST data sent via the ajax call.
+  list($form, $form_state) = ajax_get_form();
+
+  $commands = array();
   $bid = $_POST['book']['bid'];
-  if ($form = form_get_cache($_POST['form_build_id'], $cached_form_state)) {
-    // Validate the bid.
-    if (isset($form['book']['bid']['#options'][$bid])) {
-      $book_link = $form['#node']->book;
-      $book_link['bid'] = $bid;
-      // Get the new options and update the cache.
-      $form['book']['plid'] = _book_parent_select($book_link);
-      form_set_cache($_POST['form_build_id'], $form, $cached_form_state);
-      // Build and render the new select element, then return it in JSON format.
-      $form_state = array();
-      $form = form_builder($form['form_id']['#value'] , $form, $form_state);
-      $output = drupal_render($form['book']['plid']);
-      drupal_json(array('status' => TRUE, 'data' => $output));
-    }
-    else {
-      drupal_json(array('status' => FALSE, 'data' => ''));
-    }
-  }
-  else {
-    drupal_json(array('status' => FALSE, 'data' => ''));
+
+  // Validate the bid.
+  if (isset($form['book']['bid']['#options'][$bid])) {
+    $book_link = $form['#node']->book;
+    $book_link['bid'] = $bid;
+    // Get the new options and update the cache.
+    $form['book']['plid'] = _book_parent_select($book_link);
+    form_set_cache($form['values']['form_build_id'], $form, $form_state);
+
+    // Build and render the new select element, then return it in JSON format.
+    $form_state = array();
+    $form = form_builder($form['form_id']['#value'], $form, $form_state);
+
+    $commands[] = ajax_command_replace(NULL, drupal_render($form['book']['plid']));
   }
-  exit();
+
+  ajax_render($commands);
 }
diff --git modules/field/field.form.inc modules/field/field.form.inc
index f8e77e9..dbafb8d 100644
--- modules/field/field.form.inc
+++ modules/field/field.form.inc
@@ -190,7 +190,7 @@ function field_multiple_value_form($field, $instance, $items, &$form, &$form_sta
         '#value' => t('Add another item'),
         // Submit callback for disabled JavaScript.
         '#submit' => array('field_add_more_submit'),
-        '#ahah' => array(
+        '#ajax' => array(
           'path' => 'field/js_add_more/' . $bundle_name_url_css . '/' . $field_name_url_css,
           'wrapper' => $field_name_url_css . '-wrapper',
           'method' => 'replace',
@@ -362,8 +362,7 @@ function field_add_more_js($bundle_name, $field_name) {
   }
 
   if ($invalid) {
-    drupal_json(array('data' => ''));
-    exit;
+    ajax_render(array());
   }
 
   // We don't simply return a new empty widget row to append to existing ones,
@@ -434,15 +433,9 @@ function field_add_more_js($bundle_name, $field_name) {
   // Prevent duplicate wrapper.
   unset($field_form['#prefix'], $field_form['#suffix']);
 
-  // If a newly inserted widget contains AHAH behaviors, they normally won't
-  // work because AHAH doesn't know about those - it just attaches to the exact
-  // form elements that were initially specified in the Drupal.settings object.
-  // The new ones didn't exist then, so we need to update Drupal.settings
-  // by ourselves in order to let AHAH know about those new form elements.
-  $javascript = drupal_add_js(NULL, NULL);
-  $output_js = isset($javascript['setting']) ? '<script type="text/javascript">jQuery.extend(Drupal.settings, ' . drupal_to_js(call_user_func_array('array_merge_recursive', $javascript['setting'])) . ');</script>' : '';
-
-  $output = theme('status_messages') . drupal_render($field_form) . $output_js;
-  drupal_json(array('status' => TRUE, 'data' => $output));
-  exit;
+  $output = theme('status_messages') . drupal_render($field_form);
+
+  $commands = array();
+  $commands[] = ajax_command_replace(NULL, $output);
+  ajax_render($commands);
 }
diff --git modules/field/field.test modules/field/field.test
index fb46008..d4873af 100644
--- modules/field/field.test
+++ modules/field/field.test
@@ -1336,12 +1336,19 @@ class FieldFormTestCase extends DrupalWebTestCase {
     unset($this->additionalCurlOptions[CURLOPT_URL]);
 
     // The response is drupal_json, so we need to undo some escaping.
-    $response = json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->drupalGetContent()));
-    $this->assertTrue(is_object($response), t('The response is an object'));
-    $this->assertIdentical($response->status, TRUE, t('Response status is true'));
+    $commands = json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->drupalGetContent()));
+
+    // The JSON response will be two AJAX commands. The first is a settings
+    // command and the second is the replace command.
+    $settings = reset($commands);
+    $replace = next($commands);
+
+    $this->assertTrue(is_object($settings), t('The response settings command is an object'));
+    $this->assertTrue(is_object($replace), t('The response replace command is an object'));
+
     // This response data is valid HTML so we will can reuse everything we have
     // for HTML pages.
-    $this->content = $response->data;
+    $this->content = $replace->data;
 
     // Needs to be emptied out so the new content will be parsed.
     $this->elements = '';
diff --git modules/poll/poll.module modules/poll/poll.module
index b05dc70..e90afc4 100644
--- modules/poll/poll.module
+++ modules/poll/poll.module
@@ -263,14 +263,14 @@ function poll_form($node, $form_state) {
   }
 
   // We name our button 'poll_more' to avoid conflicts with other modules using
-  // AHAH-enabled buttons with the id 'more'.
+  // AJAX-enabled buttons with the id 'more'.
   $form['choice_wrapper']['poll_more'] = array(
     '#type' => 'submit',
     '#value' => t('More choices'),
     '#description' => t("If the amount of boxes above isn't enough, click here to add more choices."),
     '#weight' => 1,
     '#submit' => array('poll_more_choices_submit'), // If no javascript action.
-    '#ahah' => array(
+    '#ajax' => array(
       'callback' => 'poll_choice_js',
       'wrapper' => 'poll-choices',
       'method' => 'replace',
@@ -320,7 +320,7 @@ function poll_more_choices_submit($form, &$form_state) {
 
   // Make the changes we want to the form state.
   if ($form_state['values']['poll_more']) {
-    $n = $_GET['q'] == 'system/ahah' ? 1 : 5;
+    $n = $_GET['q'] == 'system/ajax' ? 1 : 5;
     $form_state['choice_count'] = count($form_state['values']['choice']) + $n;
   }
 }
@@ -373,9 +373,7 @@ function poll_choice_js($form, $form_state) {
 
   // Prevent duplicate wrappers.
   unset($choice_form['#prefix'], $choice_form['#suffix']);
-  $output = theme('status_messages') . drupal_render($choice_form);
-
-  drupal_json(array('status' => TRUE, 'data' => $output));
+  return theme('status_messages') . drupal_render($choice_form);
 }
 
 /**
diff --git modules/poll/poll.test modules/poll/poll.test
index 045dd4b..8500d1b 100644
--- modules/poll/poll.test
+++ modules/poll/poll.test
@@ -340,17 +340,24 @@ class PollJSAddChoice extends DrupalWebTestCase {
     // @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('system/ahah', array('absolute' => TRUE));
+    $this->additionalCurlOptions[CURLOPT_URL] = url('system/ajax', array('absolute' => TRUE));
     $this->drupalPost(NULL, $edit, t('More choices'));
     unset($this->additionalCurlOptions[CURLOPT_URL]);
 
     // The response is drupal_json, so we need to undo some escaping.
-    $response = json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->drupalGetContent()));
-    $this->assertTrue(is_object($response), t('The response is an object'));
-    $this->assertIdentical($response->status, TRUE, t('Response status is true'));
-    // This response data is valid HTML so we will can reuse everything we have
+    $commands = json_decode(str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->drupalGetContent()));
+
+    // The JSON response will be two AJAX commands. The first is a settings
+    // command and the second is the replace command.
+    $settings = reset($commands);
+    $replace = next($commands);
+
+    $this->assertTrue(is_object($settings), t('The response settings command is an object'));
+    $this->assertTrue(is_object($replace), t('The response replace command is an object'));
+
+    // This replace data is valid HTML so we will can reuse everything we have
     // for HTML pages.
-    $this->content = $response->data;
+    $this->content = $replace->data;
 
     // Needs to be emptied out so the new content will be parsed.
     $this->elements = '';
diff --git modules/system/system-rtl.css modules/system/system-rtl.css
index 76f69af..7bae1a2 100644
--- modules/system/system-rtl.css
+++ modules/system/system-rtl.css
@@ -81,10 +81,10 @@ div.teaser-button-wrapper {
 .progress-disabled {
   float: right;
 }
-.ahah-progress {
+.ajax-progress {
   float: right;
 }
-.ahah-progress .throbber {
+.ajax-progress .throbber {
   float: right;
 }
 input.password-field {
diff --git modules/system/system.css modules/system/system.css
index a464ed8..8e441cc 100644
--- modules/system/system.css
+++ modules/system/system.css
@@ -450,20 +450,20 @@ html.js .no-js {
 .progress-disabled {
   float: left; /* LTR */
 }
-.ahah-progress {
+.ajax-progress {
   float: left; /* LTR */
 }
-.ahah-progress .throbber {
+.ajax-progress .throbber {
   width: 15px;
   height: 15px;
   margin: 2px;
   background: transparent url(../../misc/throbber.gif) no-repeat 0px -18px;
   float: left; /* LTR */
 }
-tr .ahah-progress .throbber {
+tr .ajax-progress .throbber {
   margin: 0 2px;
 }
-.ahah-progress-bar {
+.ajax-progress-bar {
   width: 16em;
 }
 
diff --git modules/system/system.module modules/system/system.module
index 3274fdb..8ed3922 100644
--- modules/system/system.module
+++ modules/system/system.module
@@ -286,7 +286,7 @@ function system_elements() {
     '#name' => 'op',
     '#button_type' => 'submit',
     '#executes_submit_callback' => TRUE,
-    '#process' => array('form_process_ahah'),
+    '#process' => array('ajax_process_form'),
     '#theme_wrappers' => array('button'),
   );
 
@@ -295,7 +295,7 @@ function system_elements() {
     '#name' => 'op',
     '#button_type' => 'submit',
     '#executes_submit_callback' => FALSE,
-    '#process' => array('form_process_ahah'),
+    '#process' => array('ajax_process_form'),
     '#theme_wrappers' => array('button'),
   );
 
@@ -303,7 +303,7 @@ function system_elements() {
     '#input' => TRUE,
     '#button_type' => 'submit',
     '#executes_submit_callback' => TRUE,
-    '#process' => array('form_process_ahah'),
+    '#process' => array('ajax_process_form'),
     '#return_value' => TRUE,
     '#has_garbage_value' => TRUE,
     '#src' => NULL,
@@ -315,7 +315,7 @@ function system_elements() {
     '#size' => 60,
     '#maxlength' => 128,
     '#autocomplete_path' => FALSE,
-    '#process' => array('form_process_text_format', 'form_process_ahah'),
+    '#process' => array('form_process_text_format', 'ajax_process_form'),
     '#theme' => 'textfield',
     '#theme_wrappers' => array('form_element'),
   );
@@ -324,7 +324,7 @@ function system_elements() {
     '#input' => TRUE,
     '#size' => 60,
     '#maxlength' => 128,
-    '#process' => array('form_process_ahah'),
+    '#process' => array('ajax_process_form'),
     '#theme' => 'password',
     '#theme_wrappers' => array('form_element'),
   );
@@ -340,7 +340,7 @@ function system_elements() {
     '#cols' => 60,
     '#rows' => 5,
     '#resizable' => TRUE,
-    '#process' => array('form_process_text_format', 'form_process_ahah'),
+    '#process' => array('form_process_text_format', 'ajax_process_form'),
     '#theme' => 'textarea',
     '#theme_wrappers' => array('form_element'),
   );
@@ -355,7 +355,7 @@ function system_elements() {
   $type['radio'] = array(
     '#input' => TRUE,
     '#default_value' => NULL,
-    '#process' => array('form_process_ahah'),
+    '#process' => array('ajax_process_form'),
     '#theme' => 'radio',
     '#theme_wrappers' => array('form_element'),
     '#form_element_skip_title' => TRUE,
@@ -372,7 +372,7 @@ function system_elements() {
   $type['checkbox'] = array(
     '#input' => TRUE,
     '#return_value' => 1,
-    '#process' => array('form_process_ahah'),
+    '#process' => array('ajax_process_form'),
     '#theme' => 'checkbox',
     '#theme_wrappers' => array('form_element'),
     '#form_element_skip_title' => TRUE,
@@ -382,7 +382,7 @@ function system_elements() {
     '#input' => TRUE,
     '#size' => 0,
     '#multiple' => FALSE,
-    '#process' => array('form_process_ahah'),
+    '#process' => array('ajax_process_form'),
     '#theme' => 'select',
     '#theme_wrappers' => array('form_element'),
   );
@@ -391,7 +391,7 @@ function system_elements() {
     '#input' => TRUE,
     '#delta' => 10,
     '#default_value' => 0,
-    '#process' => array('form_process_weight', 'form_process_ahah'),
+    '#process' => array('form_process_weight', 'ajax_process_form'),
   );
 
   $type['date'] = array(
@@ -430,7 +430,7 @@ function system_elements() {
 
   $type['hidden'] = array(
     '#input' => TRUE,
-    '#process' => array('form_process_ahah'),
+    '#process' => array('ajax_process_form'),
     '#theme' => 'hidden',
   );
 
@@ -447,7 +447,7 @@ function system_elements() {
     '#collapsible' => FALSE,
     '#collapsed' => FALSE,
     '#value' => NULL,
-    '#process' => array('form_process_fieldset', 'form_process_ahah'),
+    '#process' => array('form_process_fieldset', 'ajax_process_form'),
     '#pre_render' => array('form_pre_render_fieldset'),
     '#theme_wrappers' => array('fieldset'),
   );
@@ -476,9 +476,9 @@ function system_menu() {
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
-  $items['system/ahah'] = array(
+  $items['system/ajax'] = array(
     'title' => 'AHAH callback',
-    'page callback' => 'form_ahah_callback',
+    'page callback' => 'ajax_form_callback',
     'access callback' => TRUE,
     'type' => MENU_CALLBACK,
   );
diff --git modules/upload/upload.module modules/upload/upload.module
index 1381632..738358f 100644
--- modules/upload/upload.module
+++ modules/upload/upload.module
@@ -60,7 +60,7 @@ function upload_permission() {
  */
 function upload_node_links($node, $build_mode) {
   $links = array();
-    
+
   // Display a link with the number of attachments
   $num_files = 0;
   foreach ($node->files as $file) {
@@ -582,7 +582,7 @@ function _upload_form($node) {
       '#type' => 'submit',
       '#value' => t('Attach'),
       '#name' => 'attach',
-      '#ahah' => array(
+      '#ajax' => array(
         'path' => 'upload/js',
         'wrapper' => 'attach-wrapper',
         'progress' => array('type' => 'bar', 'message' => t('Please wait...')),
@@ -692,9 +692,11 @@ function upload_js() {
   $form = form_builder('upload_js', $form, $form_state);
   $output = theme('status_messages') . drupal_render($form);
 
-  // We send the updated file attachments form.
-  // Don't call drupal_json(). ahah.js uses an iframe and
-  // the header output by drupal_json() causes problems in some browsers.
-  print drupal_to_js(array('status' => TRUE, 'data' => $output));
-  exit;
+  $commands = array();
+  $commands[] = ajax_command_replace(NULL, $output);
+
+  // AJAX uploads use an <iframe> and some browsers have problems with the
+  // 'text/javascript' Content-Type header with iframes. Passing FALSE to
+  // ajax_render() prevents the header from being sent.
+  ajax_render($commands, FALSE);
 }
