Index: includes/ajax.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/ajax.inc,v retrieving revision 1.25 diff -u -p -r1.25 ajax.inc --- includes/ajax.inc 27 Jan 2010 11:19:11 -0000 1.25 +++ includes/ajax.inc 30 Jan 2010 18:58:56 -0000 @@ -293,28 +293,12 @@ function ajax_get_form() { * The Form API #ajax property can be set both for buttons and other input * elements. * - * ajax_process_form() defines an additional 'formPath' JavaScript setting - * that is used by Drupal.ajax.prototype.beforeSubmit() to automatically inject - * an additional field 'ajax_triggering_element' to the submitted form values, - * which contains the array #parents of the element in the form structure. - * This additional field allows ajax_form_callback() to determine which - * element triggered the action, as non-submit form elements do not - * provide this information in $form_state['clicked_button'], which can - * also be used to determine triggering element, but only submit-type - * form elements. - * * This function is also the canonical example of how to implement * #ajax['path']. If processing is required that cannot be accomplished with * a callback, re-implement this function and set #ajax['path'] to the * enhanced function. */ function ajax_form_callback() { - // Find the triggering element, which was set up for us on the client side. - if (!empty($_REQUEST['ajax_triggering_element'])) { - $triggering_element_path = $_REQUEST['ajax_triggering_element']; - // Remove the value for form validation. - unset($_REQUEST['ajax_triggering_element']); - } list($form, $form_state, $form_id, $form_build_id) = ajax_get_form(); // Build, validate and if possible, submit the form. @@ -324,32 +308,11 @@ function ajax_form_callback() { // drupal_process_form() set up. $form = drupal_rebuild_form($form_id, $form_state, $form_build_id); - // $triggering_element_path in a simple form might just be 'myselect', which - // would mean we should use the element $form['myselect']. For nested form - // elements we need to recurse into the form structure to find the triggering - // element, so we can retrieve the #ajax['callback'] from it. - if (!empty($triggering_element_path)) { - if (!isset($form['#access']) || $form['#access']) { - $triggering_element = $form; - foreach (explode('/', $triggering_element_path) as $key) { - if (!empty($triggering_element[$key]) && (!isset($triggering_element[$key]['#access']) || $triggering_element[$key]['#access'])) { - $triggering_element = $triggering_element[$key]; - } - else { - // We did not find the $triggering_element or do not have #access, - // so break out and do not provide it. - $triggering_element = NULL; - break; - } - } - } - } - if (empty($triggering_element)) { - $triggering_element = $form_state['clicked_button']; - } - // Now that we have the element, get a callback if there is one. - if (!empty($triggering_element)) { - $callback = $triggering_element['#ajax']['callback']; + // As part of drupal_process_form(), the element that triggered the form + // submission is determined, and in the case of AJAX, it might not be a + // button. This lets us route to the appropriate callback. + if (!empty($form_state['triggering_element'])) { + $callback = $form_state['triggering_element']['#ajax']['callback']; } if (!empty($callback) && function_exists($callback)) { return $callback($form, $form_state); @@ -483,13 +446,43 @@ function ajax_process_form($element, &$f 'speed' => 'none', 'method' => 'replace', 'progress' => array('type' => 'throbber'), - 'formPath' => implode('/', $element['#array_parents']), ); - // Process special settings. + // Change path to url. $settings['url'] = isset($settings['path']) ? url($settings['path']) : url('system/ajax'); unset($settings['path']); - $settings['button'] = isset($element['#executes_submit_callback']) ? array($element['#name'] => $element['#value']) : FALSE; + + // Add special data to $settings['submit'] so that when this element + // triggers an AJAX submission, Drupal's form processing can determine which + // element triggered it. + if (isset($settings['trigger_as'])) { + // An element can add a 'trigger_as' key within #ajax to make the element + // submit as though another one (for example, a non-button can use this + // to submit the form as though a button were clicked). To emulate a + // button, this should be set to an array with the button name as the key + // and the button value as the value. To emulate a non-button or image + // button, this should be set to just the element's name. + if (is_array($settings['trigger_as'])) { + // Use the first key/value pair in the array. + $settings['submit']['_triggering_element_value'] = reset($settings['trigger_as']); + $settings['submit']['_triggering_element_name'] = key($settings['trigger_as']); + } + else { + $settings['submit']['_triggering_element_name'] = $settings['trigger_as']; + } + } + else { + // Most of the time, elements can submit as themselves, in which case the + // 'trigger_as' key isn't needed, and the element's name is used. + $settings['submit']['_triggering_element_name'] = $element['#name']; + // If the element is a (non-image) button, its name may not identify it + // uniquely, in which case a match on value is also needed. + // @see _form_button_was_clicked() + if (isset($element['#button_type']) && empty($element['#has_garbage_value'])) { + $settings['submit']['_triggering_element_value'] = $element['#value']; + } + } + unset($settings['trigger_as']); // Convert a simple #ajax['progress'] string into an array. if (is_string($settings['progress'])) { Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.428 diff -u -p -r1.428 form.inc --- includes/form.inc 30 Jan 2010 07:54:01 -0000 1.428 +++ includes/form.inc 30 Jan 2010 18:58:56 -0000 @@ -280,19 +280,21 @@ function form_state_defaults() { 'cache'=> FALSE, 'method' => 'post', 'groups' => array(), + 'buttons' => array(), ); } /** * Retrieves a form, caches it and processes it again. * - * If your AHAH callback simulates the pressing of a button, then your AHAH - * callback will need to do the same as what drupal_get_form would do when the + * If your AJAX callback simulates the pressing of a button, then your AJAX + * callback will need to do the same as what drupal_get_form() would do when the * button is pressed: get the form from the cache, run drupal_process_form over - * it and then if it needs rebuild, run drupal_rebuild_form over it. Then send + * it and then if it needs rebuild, run drupal_rebuild_form() over it. Then send * back a part of the returned form. - * $form_state['clicked_button']['#array_parents'] will help you to find which - * part. + * $form_state['triggering_element']['#array_parents'] will help you to find + * which part. + * @see ajax_form_callback() for an example. * * @param $form_id * The unique string identifying the desired form. If a function @@ -405,6 +407,7 @@ function form_state_keys_no_cache() { 'temporary', // Internal properties defined by form processing. 'buttons', + 'triggering_element', 'clicked_button', 'complete form', 'groups', @@ -936,22 +939,35 @@ function _form_validate(&$elements, &$fo // to form_set_error() be suppressed and not result in a form error, so // that a button that implements low-risk functionality (such as "Previous" // or "Add more") that doesn't require all user input to be valid can still - // have its submit handlers triggered. The clicked button's + // have its submit handlers triggered. The triggering element's // #limit_validation_errors property contains the information for which // errors are needed, and all other errors are to be suppressed. The - // #limit_validation_errors property is ignored if the button doesn't also - // define its own submit handlers, because it's too large a security risk to - // have any invalid user input when executing form-level submit handlers. - if (isset($form_state['clicked_button']['#limit_validation_errors']) && isset($form_state['clicked_button']['#submit'])) { - form_set_error(NULL, '', $form_state['clicked_button']['#limit_validation_errors']); - } + // #limit_validation_errors property is ignored if submit handlers will run, + // but the element doesn't have a #submit property, because it's too large a + // security risk to have any invalid user input when executing form-level + // submit handlers. + if (isset($form_state['triggering_element']['#limit_validation_errors']) && ($form_state['triggering_element']['#limit_validation_errors'] !== FALSE) && !($form_state['submitted'] && !isset($form_state['triggering_element']['#submit']))) { + form_set_error(NULL, '', $form_state['triggering_element']['#limit_validation_errors']); + } + // If submit handlers won't run (due to the submission having been triggered + // by an element whose #executes_submit_callback property isn't TRUE), then + // it's safe to suppress all validation errors, and we do so by default, + // which is particularly useful during an AJAX submission triggered by a + // non-button. An element can override this default by setting the + // #limit_validation_errors property. For button element types, + // #limit_validation_errors defaults to FALSE (via system_element_info()), + // so that full validation is their default behavior. + elseif (isset($form_state['triggering_element']) && !isset($form_state['triggering_element']['#limit_validation_errors']) && !$form_state['submitted']) { + form_set_error(NULL, '', array()); + } + // As an extra security measure, explicitly turn off error suppression if + // one of the above conditions wasn't met. Since this is also done at the + // end of this function, doing it here is only to handle the rare edge case + // where a validate handler invokes form processing of another form. else { - // As an extra security measure, explicitly turn off error suppression. - // Since this is also done at the end of this function, doing it here is - // only to handle the rare edge case where a validate handler invokes form - // processing of another form. drupal_static_reset('form_set_error:limit_validation_errors'); } + // Make sure a value is passed when the field is required. // A simple call to empty() will not cut it here as some fields, like // checkboxes, can return a valid value of '0'. Instead, check the @@ -1303,22 +1319,60 @@ function form_builder($form_id, $element $element['#after_build_done'] = TRUE; } - // Now that we've processed everything, we can go back to handle the funky - // Internet Explorer button-click scenario. - _form_builder_ie_cleanup($element, $form_state); - // If there is a file element, we need to flip a flag so later the // form encoding can be set. if (isset($element['#type']) && $element['#type'] == 'file') { $form_state['has_file_element'] = TRUE; } + // Final tasks for the form element after form_builder() has run for all other + // elements. if (isset($element['#type']) && $element['#type'] == 'form') { - // We are on the top form. // If there is a file element, we set the form encoding. if (isset($form_state['has_file_element'])) { $element['#attributes']['enctype'] = 'multipart/form-data'; } + + // Now that we've processed everything, we can go back to handle the funky + // Internet Explorer button-click scenario. + _form_builder_ie_cleanup($element, $form_state); + + // If the triggering element specifies "button-level" validation and submit + // handlers to run instead of the default form-level ones, then add those to + // the form state. + foreach (array('validate', 'submit') as $type) { + if (isset($form_state['triggering_element']['#' . $type])) { + $form_state[$type . '_handlers'] = $form_state['triggering_element']['#' . $type]; + } + } + + // If the triggering element executes submit handlers, then set the form + // state key that's needed for those handlers to run. + if (!empty($form_state['triggering_element']['#executes_submit_callback'])) { + $form_state['submitted'] = TRUE; + } + + // Special processing if the triggering element is a button. + if (isset($form_state['triggering_element']['#button_type'])) { + // Because there are several ways in which the triggering element could + // have been determined (including from input variables set by JavaScript + // or fallback behavior implemented for IE), and because buttons often + // have their #name property not derived from their #parents property, we + // can't assume that input processing that's happened up until here has + // resulted in $form_state['values'][BUTTON_NAME] being set. But it's + // common for forms to have several buttons named 'op' and switch on + // $form_state['values']['op'] during submit handler execution. + $form_state['values'][$form_state['triggering_element']['#name']] = $form_state['triggering_element']['#value']; + + // @todo To support AJAX form submissions from non-button elements, + // the older Drupal variable $form_state['clicked_button'] got + // abstracted to $form_state['triggering_element'], but this was done + // after Drupal 7 alpha release. Until we're ready to break + // compatibility with modules using $form_state['clicked_button'], we + // continue to provide this variable. + $form_state['clicked_button'] = $form_state['triggering_element']; + } + // Update the copy of the complete form for usage in validation handlers. $form_state['complete form'] = $element; } @@ -1415,52 +1469,78 @@ function _form_builder_handle_input_elem } } - // Determine which button (if any) was clicked to submit the form. - // We compare the incoming values with the buttons defined in the form, - // and flag the one that matches. We have to do some funky tricks to - // deal with Internet Explorer's handling of single-button forms, though. - if (!empty($form_state['input']) && isset($element['#executes_submit_callback'])) { - // First, accumulate a collection of buttons, divided into two bins: - // those that execute full submit callbacks and those that only validate. - $button_type = $element['#executes_submit_callback'] ? 'submit' : 'button'; - $form_state['buttons'][$button_type][] = $element; - - if (_form_button_was_clicked($element, $form_state)) { - $form_state['submitted'] = $form_state['submitted'] || $element['#executes_submit_callback']; - - // In most cases, we want to use form_set_value() to manipulate - // the global variables. In this special case, we want to make sure that - // the value of this element is listed in $form_variables under 'op'. - $form_state['values'][$element['#name']] = $element['#value']; - $form_state['clicked_button'] = $element; - - if (isset($element['#validate'])) { - $form_state['validate_handlers'] = $element['#validate']; + // Determine which element (if any) triggered the submission of the form and + // keep track of all the buttons in the form for form_state_values_clean(). + // @todo We need to add a #access check here, so that someone can't fake the + // click of a button they shouldn't have access to, but first we need to + // fix file.module's managed_file element pipeline to handle the click of + // the remove button in a submit handler instead of in a #process function. + // During the first run of form_builder() after the form is submitted, + // #process functions need to return the expanded element with child + // elements' #access properties matching what they were when the form was + // displayed to the user, since that is what we are processing input for. + // Changes to the form (like toggling the upload/remove button) need to wait + // until form rebuild. + if (!empty($form_state['input'])) { + // Detect if the element triggered the submission via AJAX. + if (_form_element_triggered_scripted_submission($element, $form_state)) { + $form_state['triggering_element'] = $element; + } + // If the form was submitted by the browser rather than via AJAX, then it + // can only have been triggered by a button, and we need to determine which + // button within the constraints of how browsers provide this information. + if (isset($element['#button_type'])) { + // All buttons in the form need to be tracked both for + // form_state_values_clean() and for _form_builder_ie_cleanup(). + // @todo When #access is checked in an outer if statement (see above), it + // won't need to be checked here. + if ($form_state['programmed'] || !isset($element['#access']) || $element['#access']) { + $form_state['buttons'][] = $element; } - if (isset($element['#submit'])) { - $form_state['submit_handlers'] = $element['#submit']; + if (_form_button_was_clicked($element, $form_state)) { + $form_state['triggering_element'] = $element; } } } + form_set_value($element, $element['#value'], $form_state); } /** - * Helper function to handle the sometimes-convoluted logic of button - * click detection. + * Helper function to handle the sometimes-convoluted logic of button click detection. + * + * This detects button or non-button controls that trigger a form submission via + * AJAX or some other scriptable environment. These environments can set the + * special input key '_triggering_element_name' to identify the triggering + * element. If the name alone doesn't identify the element uniquely, the input + * key '_triggering_element_value' may also be set to require a match on element + * value. An example where this is needed is if there are several buttons all + * named 'op', and only differing in their value. + */ +function _form_element_triggered_scripted_submission($element, &$form_state) { + if (!empty($form_state['input']['_triggering_element_name']) && $element['#name'] == $form_state['input']['_triggering_element_name']) { + if (empty($form_state['input']['_triggering_element_value']) || $form_state['input']['_triggering_element_value'] == $element['#value']) { + return TRUE; + } + } + return FALSE; +} + +/** + * Helper function to handle the sometimes-convoluted logic of button click detection. * * In Internet Explorer, if ONLY one submit button is present, AND the * enter key is used to submit the form, no form value is sent for it * and we'll never detect a match. That special case is handled by * _form_builder_ie_cleanup(). */ -function _form_button_was_clicked($form, &$form_state) { +function _form_button_was_clicked($element, &$form_state) { // First detect normal 'vanilla' button clicks. Traditionally, all // standard buttons on a form share the same name (usually 'op'), // and the specific return value is used to determine which was // clicked. This ONLY works as long as $form['#name'] puts the // value at the top level of the tree of $_POST data. - if (isset($form_state['input'][$form['#name']]) && $form_state['input'][$form['#name']] == $form['#value']) { + if (isset($form_state['input'][$element['#name']]) && $form_state['input'][$element['#name']] == $element['#value']) { return TRUE; } // When image buttons are clicked, browsers do NOT pass the form element @@ -1468,38 +1548,22 @@ function _form_button_was_clicked($form, // coordinates of the click on the button image. This means that image // buttons MUST have unique $form['#name'] values, but the details of // their $_POST data should be ignored. - elseif (!empty($form['#has_garbage_value']) && isset($form['#value']) && $form['#value'] !== '') { + elseif (!empty($element['#has_garbage_value']) && isset($element['#value']) && $element['#value'] !== '') { return TRUE; } return FALSE; } /** - * In IE, if only one submit button is present, AND the enter key is - * used to submit the form, no form value is sent for it and our normal - * button detection code will never detect a match. We call this - * function after all other button-detection is complete to check - * for the proper conditions, and treat the single button on the form - * as 'clicked' if they are met. + * In IE, if only one submit button is present, AND the enter key is used to + * submit the form, no form value is sent for it and our normal button detection + * code will never detect a match. This function is called after all other + * button-detection is complete to check for the proper conditions, and treat + * the single button on the form as 'clicked' if they are met. */ function _form_builder_ie_cleanup($form, &$form_state) { - // Quick check to make sure we're always looking at the full form - // and not a sub-element. - if (!empty($form['#type']) && $form['#type'] == 'form') { - // If we haven't recognized a submission yet, and there's a single - // submit button, we know that we've hit the right conditions. Grab - // the first one and treat it as the clicked button. - if (empty($form_state['submitted']) && !empty($form_state['buttons']['submit']) && empty($form_state['buttons']['button'])) { - $button = $form_state['buttons']['submit'][0]; - - // Set up all the $form_state information that would have been - // populated had the button been recognized earlier. - $form_state['submitted'] = TRUE; - $form_state['submit_handlers'] = empty($button['#submit']) ? NULL : $button['#submit']; - $form_state['validate_handlers'] = empty($button['#validate']) ? NULL : $button['#validate']; - $form_state['values'][$button['#name']] = $button['#value']; - $form_state['clicked_button'] = $button; - } + if (!$form_state['programmed'] && !isset($form_state['triggering_element']) && count($form_state['buttons']) == 1) { + $form_state['triggering_element'] = $form_state['buttons'][0]; } } @@ -1526,37 +1590,35 @@ function form_state_values_clean(&$form_ unset($form_state['values']['form_id'], $form_state['values']['form_token'], $form_state['values']['form_build_id'], $form_state['values']['op']); // Remove button values. - // form_builder() collects all button elements in a form, keyed by button - // type. We remove the button value separately for each button element. - foreach ($form_state['buttons'] as $button_type => $buttons) { - foreach ($buttons as $button) { - // Remove this button's value from the submitted form values by finding - // the value corresponding to this button. - // We iterate over the #parents of this button and move a reference to - // each parent in $form_state['values']. For example, if #parents is: - // array('foo', 'bar', 'baz') - // then the corresponding $form_state['values'] part will look like this: - // array( - // 'foo' => array( - // 'bar' => array( - // 'baz' => 'button_value', - // ), - // ), - // ) - // We start by (re)moving 'baz' to $last_parent, so we are able unset it - // at the end of the iteration. Initially, $values will contain a - // reference to $form_state['values'], but in the iteration we move the - // reference to $form_state['values']['foo'], and finally to - // $form_state['values']['foo']['bar'], which is the level where we can - // unset 'baz' (that is stored in $last_parent). - $parents = $button['#parents']; - $values = &$form_state['values']; - $last_parent = array_pop($parents); - foreach ($parents as $parent) { - $values = &$values[$parent]; - } - unset($values[$last_parent]); + // form_builder() collects all button elements in a form. We remove the button + // value separately for each button element. + foreach ($form_state['buttons'] as $button) { + // Remove this button's value from the submitted form values by finding + // the value corresponding to this button. + // We iterate over the #parents of this button and move a reference to + // each parent in $form_state['values']. For example, if #parents is: + // array('foo', 'bar', 'baz') + // then the corresponding $form_state['values'] part will look like this: + // array( + // 'foo' => array( + // 'bar' => array( + // 'baz' => 'button_value', + // ), + // ), + // ) + // We start by (re)moving 'baz' to $last_parent, so we are able unset it + // at the end of the iteration. Initially, $values will contain a + // reference to $form_state['values'], but in the iteration we move the + // reference to $form_state['values']['foo'], and finally to + // $form_state['values']['foo']['bar'], which is the level where we can + // unset 'baz' (that is stored in $last_parent). + $parents = $button['#parents']; + $values = &$form_state['values']; + $last_parent = array_pop($parents); + foreach ($parents as $parent) { + $values = &$values[$parent]; } + unset($values[$last_parent]); } } Index: misc/ajax.js =================================================================== RCS file: /cvs/drupal/drupal/misc/ajax.js,v retrieving revision 1.9 diff -u -p -r1.9 ajax.js --- misc/ajax.js 12 Jan 2010 06:31:22 -0000 1.9 +++ misc/ajax.js 30 Jan 2010 18:58:56 -0000 @@ -98,7 +98,7 @@ Drupal.ajax = function (base, element, e type: 'bar', message: 'Please wait...' }, - button: {} + submit: {} }; $.extend(this, defaults, element_settings); @@ -121,7 +121,7 @@ Drupal.ajax = function (base, element, e var ajax = this; var options = { url: ajax.url, - data: ajax.button, + data: ajax.submit, beforeSerialize: function (element_settings, options) { return ajax.beforeSerialize(element_settings, options); }, @@ -200,10 +200,6 @@ Drupal.ajax.prototype.beforeSubmit = fun // Disable the element that received the change. $(this.element).addClass('progress-disabled').attr('disabled', true); - // Server-side code needs to know what element triggered the call, so it can - // find the #ajax binding. - form_values.push({ name: 'ajax_triggering_element', value: this.formPath }); - // 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)); Index: modules/file/file.module =================================================================== RCS file: /cvs/drupal/drupal/modules/file/file.module,v retrieving revision 1.18 diff -u -p -r1.18 file.module --- modules/file/file.module 28 Jan 2010 13:56:25 -0000 1.18 +++ modules/file/file.module 30 Jan 2010 18:58:57 -0000 @@ -385,12 +385,12 @@ function file_managed_file_process($elem '#weight' => -5, ); - // Because the output of this field changes depending on the button clicked, - // we need to ask FAPI immediately if the remove button was clicked. - // It's not good that we call this private function, but - // $form_state['clicked_button'] is only available after this #process - // callback is finished. - if (_form_button_was_clicked($element['remove_button'], $form_state)) { + // @todo It is not good to call these private functions. This should be + // refactored so that the file deletion happens during a submit handler, + // and form changes affected by that (such as toggling the upload and remove + // buttons) happens during the 2nd run of this function that is triggered by + // a form rebuild. + if (_form_button_was_clicked($element['remove_button'], $form_state) || _form_element_triggered_scripted_submission($element['remove_button'], $form_state)) { // If it's a temporary file we can safely remove it immediately, otherwise // it's up to the implementing module to clean up files that are in use. if ($element['#file'] && $element['#file']->status == 0) { Index: modules/poll/poll.test =================================================================== RCS file: /cvs/drupal/drupal/modules/poll/poll.test,v retrieving revision 1.28 diff -u -p -r1.28 poll.test --- modules/poll/poll.test 9 Jan 2010 21:54:01 -0000 1.28 +++ modules/poll/poll.test 30 Jan 2010 18:58:57 -0000 @@ -341,7 +341,7 @@ class PollJSAddChoice extends DrupalWebT // Press 'add choice' button through AJAX, and place the expected HTML result // as the tested content. - $commands = $this->drupalPostAJAX(NULL, $edit, 'poll_more'); + $commands = $this->drupalPostAJAX(NULL, $edit, array('op' => t('More choices'))); $this->content = $commands[1]['data']; $this->assertFieldByName('choice[chid:0][chtext]', $edit['choice[new:0][chtext]'], t('Field !i found', array('!i' => 0))); Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.193 diff -u -p -r1.193 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 15 Jan 2010 03:07:34 -0000 1.193 +++ modules/simpletest/drupal_web_test_case.php 30 Jan 2010 18:58:57 -0000 @@ -1519,11 +1519,13 @@ class DrupalWebTestCase extends DrupalTe * which is likely different than the $path parameter used for retrieving * the initial form. Defaults to 'system/ajax'. * - triggering_element: If the value for the 'path' key is 'system/ajax' or - * another generic AJAX processing path, this needs to be set to the '/' - * separated path to the element within the server's cached $form array. - * The callback for the generic AJAX processing path uses this to find - * the #ajax information for the element, including which specific - * callback to use for processing the request. + * another generic AJAX processing path, this needs to be set to the name + * of the element. If the name doesn't identify the element uniquely, then + * this should instead be an array with a single key/value pair, + * corresponding to the element name and value. The callback for the + * generic AJAX processing path uses this to find the #ajax information + * for the element, including which specific callback to use for + * processing the request. * @param $options * Options to be forwarded to url(). * @param $headers @@ -1578,7 +1580,14 @@ class DrupalWebTestCase extends DrupalTe $post[$key] = urlencode($key) . '=' . urlencode($value); } if ($ajax && isset($submit['triggering_element'])) { - $post['ajax_triggering_element'] = 'ajax_triggering_element=' . urlencode($submit['triggering_element']); + if (is_array($submit['triggering_element'])) { + // Get the first key/value pair in the array. + $post['_triggering_element_value'] = '_triggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $post['_triggering_element_name'] = '_triggering_element_name=' . urlencode(key($submit['triggering_element'])); + } + else { + $post['_triggering_element_name'] = '_triggering_element_name=' . urlencode($submit['triggering_element']); + } } $post = implode('&', $post); } Index: modules/simpletest/tests/ajax.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/ajax.test,v retrieving revision 1.5 diff -u -p -r1.5 ajax.test --- modules/simpletest/tests/ajax.test 12 Dec 2009 23:36:28 -0000 1.5 +++ modules/simpletest/tests/ajax.test 30 Jan 2010 18:58:57 -0000 @@ -83,63 +83,63 @@ class AJAXCommandsTestCase extends AJAXT $edit = array(); // Tests the 'after' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'after_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'After': Click to put something after the div"))); $command = $commands[1]; $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'after' && $command['data'] == 'This will be placed after', "'after' AJAX command issued with correct data"); // Tests the 'alert' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'alert_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'Alert': Click to alert"))); $command = $commands[1]; $this->assertTrue($command['command'] == 'alert' && $command['text'] == 'Alert', "'alert' AJAX Command issued with correct text"); // Tests the 'append' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'append_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'Append': Click to append something"))); $command = $commands[1]; $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'append' && $command['data'] == 'Appended text', "'append' AJAX command issued with correct data"); // Tests the 'before' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'before_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'before': Click to put something before the div"))); $command = $commands[1]; $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'before' && $command['data'] == 'Before text', "'before' AJAX command issued with correct data"); // Tests the 'changed' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'changed_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed."))); $command = $commands[1]; $this->assertTrue($command['command'] == 'changed' && $command['selector'] == '#changed_div', "'changed' AJAX command issued with correct selector"); // Tests the 'changed' command using the second argument. - $commands = $this->drupalPostAJAX($form_path, $edit, 'changed_command_asterisk_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed with asterisk."))); $command = $commands[1]; $this->assertTrue($command['command'] == 'changed' && $command['selector'] == '#changed_div' && $command['asterisk'] == '#changed_div_mark_this', "'changed' AJAX command (with asterisk) issued with correct selector"); // Tests the 'css' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'css_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the the '#box' div to be blue."))); $command = $commands[1]; $this->assertTrue($command['command'] == 'css' && $command['selector'] == '#css_div' && $command['argument']['background-color'] == 'blue', "'css' AJAX command issued with correct selector"); // Tests the 'data' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'data_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX data command: Issue command."))); $command = $commands[1]; $this->assertTrue($command['command'] == 'data' && $command['name'] == 'testkey' && $command['value'] == 'testvalue', "'data' AJAX command issued with correct key and value"); // Tests the 'html' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'html_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX html: Replace the HTML in a selector."))); $command = $commands[1]; $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'html' && $command['data'] == 'replacement text', "'html' AJAX command issued with correct data"); // Tests the 'prepend' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'prepend_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'prepend': Click to prepend something"))); $command = $commands[1]; $this->assertTrue($command['command'] == 'insert' && $command['method'] == 'prepend' && $command['data'] == 'prepended text', "'prepend' AJAX command issued with correct data"); // Tests the 'remove' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'remove_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'remove': Click to remove text"))); $command = $commands[1]; $this->assertTrue($command['command'] == 'remove' && $command['selector'] == '#remove_text', "'remove' AJAX command issued with correct command and selector"); // Tests the 'restripe' command. - $commands = $this->drupalPostAJAX($form_path, $edit, 'restripe_command_example'); + $commands = $this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'restripe' command"))); $command = $commands[1]; $this->assertTrue($command['command'] == 'restripe' && $command['selector'] == '#restripe_table', "'restripe' AJAX command issued with correct selector"); } Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.881 diff -u -p -r1.881 system.module --- modules/system/system.module 30 Jan 2010 07:54:01 -0000 1.881 +++ modules/system/system.module 30 Jan 2010 18:58:58 -0000 @@ -327,6 +327,7 @@ function system_element_info() { '#name' => 'op', '#button_type' => 'submit', '#executes_submit_callback' => TRUE, + '#limit_validation_errors' => FALSE, '#process' => array('ajax_process_form'), '#theme_wrappers' => array('button'), ); @@ -335,6 +336,7 @@ function system_element_info() { '#name' => 'op', '#button_type' => 'submit', '#executes_submit_callback' => FALSE, + '#limit_validation_errors' => FALSE, '#process' => array('ajax_process_form'), '#theme_wrappers' => array('button'), ); @@ -342,6 +344,7 @@ function system_element_info() { '#input' => TRUE, '#button_type' => 'submit', '#executes_submit_callback' => TRUE, + '#limit_validation_errors' => FALSE, '#process' => array('ajax_process_form'), '#return_value' => TRUE, '#has_garbage_value' => TRUE,