diff --git a/includes/ajax.inc b/includes/ajax.inc index 1526411d1149d80be21e54f4a324cc5eea58a582..fbf1da89e0ec7c716dc5951c455797c2afa792ec 100644 --- a/includes/ajax.inc +++ b/includes/ajax.inc @@ -257,28 +257,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. @@ -288,32 +272,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); @@ -499,13 +462,40 @@ function ajax_process_form($element, &$form_state) { '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. + // @see _form_element_triggered_scripted_submission() + 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). When using this, + // the 'name' key is always required to identify the element to trigger + // as. The 'value' key is optional, and only needed when multiple elements + // share the same name, which is commonly the case for buttons. + $settings['submit']['_triggering_element_name'] = $settings['trigger_as']['name']; + if (isset($settings['trigger_as']['value'])) { + $settings['submit']['_triggering_element_value'] = $settings['trigger_as']['value']; + } + unset($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']; + } + } // Convert a simple #ajax['progress'] string into an array. if (is_string($settings['progress'])) { diff --git a/includes/form.inc b/includes/form.inc index fe14c083b3c5622261f7eae75d0f4dfe16dc6c6d..bc6dc4865d9a90c759d600aa881f9b5b981244b2 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -281,19 +281,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 @@ -403,6 +405,7 @@ function form_state_keys_no_cache() { 'temporary', // Internal properties defined by form processing. 'buttons', + 'triggering_element', 'clicked_button', 'complete form', 'groups', @@ -935,22 +938,35 @@ function _form_validate(&$elements, &$form_state, $form_id = NULL) { // 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 @@ -1296,22 +1312,61 @@ function form_builder($form_id, $element, &$form_state) { $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'; } + + // If a form contains a single textfield, and the ENTER key is pressed + // within it, Internet Explorer submits the form with no POST data + // identifying any submit button. Other browsers submit POST data as though + // the user clicked the first button. Therefore, to be as consistent as we + // can be across browsers, if no 'triggering_element' has been identified + // yet, default it to the first button. + if (!$form_state['programmed'] && !isset($form_state['triggering_element']) && !empty($form_state['buttons'])) { + $form_state['triggering_element'] = $form_state['buttons'][0]; + } + + // 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 Legacy support. Remove in Drupal 8. + $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; } @@ -1408,33 +1463,42 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) { } } - // 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: http://drupal.org/node/736298. + 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 for + // form_state_values_clean() and for the form_builder() code that handles + // a form submission containing no button information in $_POST. + // @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; } } } + // Set the element's value in $form_state['values'], but only, if its key // does not exist yet (a #value_callback may have already populated it). $values = $form_state['values']; @@ -1447,21 +1511,52 @@ function _form_builder_handle_input_element($form_id, &$element, &$form_state) { } /** - * Helper function to handle the sometimes-convoluted logic of button - * click detection. + * Helper function to handle the 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(). + * 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_button_was_clicked($form, &$form_state) { +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 convoluted logic of button click detection. + * + * This detects button controls that trigger a form submission by being clicked + * and having the click processed by the browser rather than being captured by + * JavaScript. Essentially, it detects if the button's name and value are part + * of the POST data, but with extra code to deal with the convoluted way in + * which browsers submit data for image button clicks. + * + * This does not detect button clicks processed by AJAX (that is done in + * _form_element_triggered_scripted_submission()) and it does not detect form + * submissions from Internet Explorer in response to an ENTER key pressed in a + * textfield (form_builder() has extra code for that). + * + * Because this function contains only part of the logic needed to determine + * $form_state['triggering_element'], it should not be called from anywhere + * other than within the Form API. Form validation and submit handlers needing + * to know which button was clicked should get that information from + * $form_state['triggering_element']. + */ +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 @@ -1469,41 +1564,12 @@ function _form_button_was_clicked($form, &$form_state) { // 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. - */ -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; - } - } -} - /** * Removes internal Form API elements and buttons from submitted form values. * @@ -1527,37 +1593,35 @@ function form_state_values_clean(&$form_state) { 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]); } } diff --git a/misc/ajax.js b/misc/ajax.js index 3d0357cbc2ab50cc67fce2da145e6181d051e329..23179fd07d4942c57e09468d9e8b664dc3780c15 100644 --- a/misc/ajax.js +++ b/misc/ajax.js @@ -98,7 +98,7 @@ Drupal.ajax = function (base, element, element_settings) { type: 'bar', message: 'Please wait...' }, - button: {} + submit: {} }; $.extend(this, defaults, element_settings); @@ -121,7 +121,7 @@ Drupal.ajax = function (base, element, element_settings) { 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 = function (form_values, element, options) { // 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)); diff --git a/modules/file/file.module b/modules/file/file.module index d0f95cfb8362acacbb46a8dcc0d374c2cd82befd..0eba8471560932caba2ca0dea6231443a8ee3dd6 100644 --- a/modules/file/file.module +++ b/modules/file/file.module @@ -385,12 +385,12 @@ function file_managed_file_process($element, &$form_state, $form) { '#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: http://drupal.org/node/736298. + 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) { diff --git a/modules/overlay/overlay.module b/modules/overlay/overlay.module index eeb9096bfa1eff17cbacac4e3a861124514204bd..b5ec993b8b70f63ca618e8c6d9fd63a4bb6bef6b 100644 --- a/modules/overlay/overlay.module +++ b/modules/overlay/overlay.module @@ -317,8 +317,6 @@ function overlay_preprocess_toolbar(&$variables) { * processing, so that it's possible to close the overlay after submitting * a form. * - * @see _form_builder_handle_input_element() - * @see _form_builder_ie_cleanup() * @see form_execute_handlers() * @see form_builder() * @see overlay_form_submit() @@ -327,17 +325,6 @@ function overlay_preprocess_toolbar(&$variables) { */ function overlay_form_after_build($form, &$form_state) { if (overlay_get_mode() == 'child') { - // Form API may have already captured submit handlers from the submitted - // button before after_build callback is invoked. This may have been done - // by _form_builder_handle_input_element(). If so, the list of submit - // handlers is stored in the $form_state array, which is something we can - // also alter from here, luckily. Rememeber: our goal here is to set - // $form_state['redirect'] to FALSE if the API function - // overlay_request_dialog_close() has been invoked. That's because we want - // to tell the parent window to close the overlay. - if (!empty($form_state['submit_handlers']) && !in_array('overlay_form_submit', $form_state['submit_handlers'])) { - $form_state['submit_handlers'][] = 'overlay_form_submit'; - } // If this element has submit handlers, then append our own. if (isset($form['#submit'])) { $form['#submit'][] = 'overlay_form_submit'; diff --git a/modules/poll/poll.test b/modules/poll/poll.test index 2b5b43efb5c4d8c935b48175e3b44e947d4bbb75..e4a7db8488e3642957d4ae25c5d9a8f9e1da26a9 100644 --- a/modules/poll/poll.test +++ b/modules/poll/poll.test @@ -341,7 +341,7 @@ class PollJSAddChoice extends DrupalWebTestCase { // 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))); diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index f2622444d6b63177a3d6764fd3c4bf7eabdefb81..42ac8305830dbe5d1c5c8812f3adf657bf6b7833 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -1576,11 +1576,18 @@ protected function drupalGetAJAX($path, array $options = array(), array $headers * 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. + * + * This can also be set to NULL in order to emulate an Internet Explorer + * submission of a form with a single text field, and pressing ENTER in that + * textfield: under these conditions, no button information is added to the + * POST data. * @param $options * Options to be forwarded to url(). * @param $headers @@ -1622,7 +1629,7 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a // We post only if we managed to handle every field in edit and the // submit button matches. - if (!$edit && $submit_matches) { + if (!$edit && ($submit_matches || !isset($submit))) { $post_array = $post; if ($upload) { // TODO: cURL handles file uploads for us, but the implementation @@ -1643,7 +1650,14 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a $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); } @@ -1666,7 +1680,7 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a foreach ($edit as $name => $value) { $this->fail(t('Failed to set field @name to @value', array('@name' => $name, '@value' => $value))); } - if (!$ajax) { + if (!$ajax && isset($submit)) { $this->assertTrue($submit_matches, t('Found the @submit button', array('@submit' => $submit))); } $this->fail(t('Found the requested form fields at @path', array('@path' => $path))); @@ -1856,7 +1870,7 @@ protected function handleForm(&$post, &$edit, &$upload, $submit, $form) { break; case 'submit': case 'image': - if ($submit == $value) { + if (isset($submit) && $submit == $value) { $post[$name] = $value; $submit_matches = TRUE; } diff --git a/modules/simpletest/tests/ajax.test b/modules/simpletest/tests/ajax.test index d959a4defd6fac6b13a59a5abb8e28dbcc009639..ca6f718dd22afcbbf2684fd25163a8d40d3452fb 100644 --- a/modules/simpletest/tests/ajax.test +++ b/modules/simpletest/tests/ajax.test @@ -98,62 +98,62 @@ class AJAXCommandsTestCase extends AJAXTestCase { $edit = array(); // Tests the 'after' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, 'after_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'After': Click to put something after the div")))); $command = $commands[0]; $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->discardSettings($this->drupalPostAJAX($form_path, $edit, 'alert_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'Alert': Click to alert")))); $command = $commands[0]; $this->assertTrue($command['command'] == 'alert' && $command['text'] == 'Alert', "'alert' AJAX Command issued with correct text"); // Tests the 'append' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, 'append_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'Append': Click to append something")))); $command = $commands[0]; $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->discardSettings($this->drupalPostAJAX($form_path, $edit, 'before_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'before': Click to put something before the div")))); $command = $commands[0]; $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->discardSettings($this->drupalPostAJAX($form_path, $edit, 'changed_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed.")))); $command = $commands[0]; $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->discardSettings($this->drupalPostAJAX($form_path, $edit, 'changed_command_asterisk_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX changed: Click to mark div changed with asterisk.")))); $command = $commands[0]; $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->discardSettings($this->drupalPostAJAX($form_path, $edit, 'css_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("Set the the '#box' div to be blue.")))); $command = $commands[0]; $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->discardSettings($this->drupalPostAJAX($form_path, $edit, 'data_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX data command: Issue command.")))); $command = $commands[0]; $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->discardSettings($this->drupalPostAJAX($form_path, $edit, 'html_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX html: Replace the HTML in a selector.")))); $command = $commands[0]; $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->discardSettings($this->drupalPostAJAX($form_path, $edit, 'prepend_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'prepend': Click to prepend something")))); $command = $commands[0]; $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->discardSettings($this->drupalPostAJAX($form_path, $edit, 'remove_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'remove': Click to remove text")))); $command = $commands[0]; $this->assertTrue($command['command'] == 'remove' && $command['selector'] == '#remove_text', "'remove' AJAX command issued with correct command and selector"); // Tests the 'restripe' command. - $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, 'restripe_command_example')); + $commands = $this->discardSettings($this->drupalPostAJAX($form_path, $edit, array('op' => t("AJAX 'restripe' command")))); $command = $commands[0]; $this->assertTrue($command['command'] == 'restripe' && $command['selector'] == '#restripe_table', "'restripe' AJAX command issued with correct selector"); } @@ -203,3 +203,37 @@ class AJAXFormValuesTestCase extends AJAXTestCase { } } } + + +/** + * Miscellaneous AJAX tests using ajax_test module. + */ +class AJAXElementValidation extends AJAXTestCase { + public static function getInfo() { + return array( + 'name' => 'Miscellaneous AJAX tests', + 'description' => 'Various tests of AJAX behavior', + 'group' => 'AJAX', + ); + } + + /** + * Try to post an AJAX change to a form that has a validated element. + * + * The drivertext field is AJAX-enabled. An additional field is not, but + * is set to be a required field. In this test the required field is not + * filled in, and we want to see if the activation of the "drivertext" + * AJAX-enabled field fails due to the required field being empty. + */ + function testAJAXElementValidation() { + $web_user = $this->drupalCreateUser(); + $edit = array('drivertext' => t('some dumb text')); + + // Post with 'drivertext' as the triggering element. + $post_result = $this->drupalPostAJAX('ajax_validation_test', $edit, 'drivertext'); + // Look for a validation failure in the resultant JSON. + $this->assertNoText(t('Error message'), t("No error message in resultant JSON")); + $this->assertText('ajax_forms_test_validation_form_callback invoked', t('The correct callback was invoked')); + } +} + diff --git a/modules/simpletest/tests/ajax_forms_test.module b/modules/simpletest/tests/ajax_forms_test.module index 040a1d9b8898f3caad439c44f48d805a29abc6f1..5de2b842c694aac4fd5bb708b1fe6011b8a565da 100644 --- a/modules/simpletest/tests/ajax_forms_test.module +++ b/modules/simpletest/tests/ajax_forms_test.module @@ -24,6 +24,12 @@ function ajax_forms_test_menu() { 'page arguments' => array('ajax_forms_test_ajax_commands_form'), 'access callback' => TRUE, ); + $items['ajax_validation_test'] = array( + 'title' => 'AJAX Validation Test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('ajax_forms_test_validation_form'), + 'access callback' => TRUE, + ); return $items; } @@ -340,3 +346,59 @@ function ajax_forms_test_advanced_commands_restripe_callback($form, $form_state) $commands[] = ajax_command_restripe('#restripe_table'); return array('#type' => 'ajax', '#commands' => $commands); } + + + +/** + * This form and its related submit and callback functions demonstrate + * not validating another form element when a single AJAX element is triggered. + * + * The "drivertext" element is an AJAX-enabled textfield, free-form. + * The "required_field" element is a textfield marked required. + * + * The correct behavior is that the AJAX-enabled drivertext element should + * be able to trigger without causing validation of the "required_field". + */ +function ajax_forms_test_validation_form($form, &$form_state) { + + $form['drivertext'] = array( + '#title' => t('AJAX-enabled textfield.'), + '#description' => t("When this one AJAX-triggers and the spare required field is empty, you should not get an error."), + '#type' => 'textfield', + '#default_value' => !empty($form_state['values']['drivertext']) ? $form_state['values']['drivertext'] : "", + '#ajax' => array( + 'callback' => 'ajax_forms_test_validation_form_callback', + 'wrapper' => 'message_area', + 'method' => 'replace', + ), + '#suffix' => '<div id="message_area"></div>', + ); + + $form['spare_required_field'] = array( + '#title' => t("Spare Required Field"), + '#type' => 'textfield', + '#required' => TRUE, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + return $form; +} +/** + * Submit handler for the validation form. + */ +function ajax_forms_test_validation_form_submit($form, $form_state) { + drupal_set_message(t("Validation form submitted")); +} + +/** + * AJAX callback for the 'drivertext' element of the validation form. + */ +function ajax_forms_test_validation_form_callback($form, $form_state) { + drupal_set_message("ajax_forms_test_validation_form_callback invoked"); + drupal_set_message(t("Callback: drivertext=%drivertext, spare_required_field=%spare_required_field", array('%drivertext' => $form_state['values']['drivertext'], '%spare_required_field' => $form_state['values']['spare_required_field']))); + return '<div id="message_area">ajax_forms_test_validation_form_callback at ' . date('c') . '</div>'; +} diff --git a/modules/simpletest/tests/form.test b/modules/simpletest/tests/form.test index 3c447d14d81aaba529db47620339140f739dce2b..4683fb8d4ebdf75ba585a5a6022f253c183b2470 100644 --- a/modules/simpletest/tests/form.test +++ b/modules/simpletest/tests/form.test @@ -850,6 +850,77 @@ class FormsProgrammaticTestCase extends DrupalWebTestCase { } } +/** + * Test that FAPI correctly determines $form_state['clicked_button']. + */ +class FormsClickedButtonTestCase extends DrupalWebTestCase { + + function getInfo() { + return array( + 'name' => 'Form clicked button determination', + 'description' => 'Test the determination of $form_state[\'clicked_button\'].', + 'group' => 'Form API', + ); + } + + function setUp() { + parent::setUp('form_test'); + } + + /** + * Test the determination of $form_state['clicked_button'] when no button + * information is included in the POST data, as is sometimes the case when + * the ENTER key is pressed in a textfield in Internet Explorer. + */ + function testNoButtonInfoInPost() { + $path = 'form-test/clicked-button'; + $edit = array(); + $form_id = 'form-test-clicked-button'; + + // Ensure submitting a form with no buttons results in no + // $form_state['clicked_button'] and the form submit handler not running. + drupal_static_reset('drupal_html_id'); + $this->drupalPost($path, $edit, NULL, array(), array(), $form_id); + $this->assertText('There is no clicked button.', t('$form_state[\'clicked_button\'] set to NULL.')); + $this->assertNoText('Submit handler for form_test_clicked_button executed.', t('Form submit handler did not execute.')); + + // Ensure submitting a form with one or more submit buttons results in + // $form_state['clicked_button'] being set to the first one the user has + // access to. An argument with 'r' in it indicates a restricted + // (#access=FALSE) button. + drupal_static_reset('drupal_html_id'); + $this->drupalPost($path . '/s', $edit, NULL, array(), array(), $form_id); + $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to only button.')); + $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.')); + drupal_static_reset('drupal_html_id'); + $this->drupalPost($path . '/s/s', $edit, NULL, array(), array(), $form_id); + $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.')); + $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.')); + drupal_static_reset('drupal_html_id'); + $this->drupalPost($path . '/rs/s', $edit, NULL, array(), array(), $form_id); + $this->assertText('The clicked button is button2.', t('$form_state[\'clicked_button\'] set to first available button.')); + $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.')); + + // Ensure submitting a form with buttons of different types results in + // $form_state['clicked_button'] being set to the first button, regardless + // of type. For the FAPI 'button' type, this should result in the submit + // handler not executing. The types are 's'(ubmit), 'b'(utton), and + // 'i'(mage_button). + drupal_static_reset('drupal_html_id'); + $this->drupalPost($path . '/s/b/i', $edit, NULL, array(), array(), $form_id); + $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.')); + $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.')); + drupal_static_reset('drupal_html_id'); + $this->drupalPost($path . '/b/s/i', $edit, NULL, array(), array(), $form_id); + $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.')); + $this->assertNoText('Submit handler for form_test_clicked_button executed.', t('Form submit handler did not execute.')); + drupal_static_reset('drupal_html_id'); + $this->drupalPost($path . '/i/s/b', $edit, NULL, array(), array(), $form_id); + $this->assertText('The clicked button is button1.', t('$form_state[\'clicked_button\'] set to first button.')); + $this->assertText('Submit handler for form_test_clicked_button executed.', t('Form submit handler executed.')); + } +} + /** * Tests rebuilding of arbitrary forms by altering them. diff --git a/modules/simpletest/tests/form_test.module b/modules/simpletest/tests/form_test.module index ad9be7032250fb8d5346b9bf965253bf9faf6268..b35e4325172c4c3709352ba11849d4a66b6e2af4 100644 --- a/modules/simpletest/tests/form_test.module +++ b/modules/simpletest/tests/form_test.module @@ -118,6 +118,14 @@ function form_test_menu() { 'type' => MENU_CALLBACK, ); + $items['form-test/clicked-button'] = array( + 'title' => 'Clicked button test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('form_test_clicked_button'), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -923,6 +931,84 @@ function form_test_programmatic_form_submit($form, &$form_state) { $form_state['storage']['programmatic_form_submit'] = $form_state['values']['submitted_field']; } +/** + * Form builder to test button click detection. + */ +function form_test_clicked_button($form, &$form_state) { + // A single text field. In IE, when a form has only one non-button input field + // and the ENTER key is pressed while that field has focus, the form is + // submitted without any information identifying the button responsible for + // the submission. In other browsers, the form is submitted as though the + // first button were clicked. + $form['text'] = array( + '#title' => 'Text', + '#type' => 'textfield', + ); + + // Loop through each path argument, addding buttons based on the information + // in the argument. For example, if the path is + // form-test/clicked-button/s/i/rb, then 3 buttons are added: a 'submit', an + // 'image_button', and a 'button' with #access=FALSE. This enables form.test + // to test a variety of combinations. + $i=0; + $args = array_slice(arg(), 2); + foreach ($args as $arg) { + $name = 'button' . ++$i; + // 's', 'b', or 'i' in the argument define the button type wanted. + if (strpos($arg, 's') !== FALSE) { + $type = 'submit'; + } + elseif (strpos($arg, 'b') !== FALSE) { + $type = 'button'; + } + elseif (strpos($arg, 'i') !== FALSE) { + $type = 'image_button'; + } + else { + $type = NULL; + } + if (isset($type)) { + $form[$name] = array( + '#type' => $type, + '#name' => $name, + ); + // Image buttons need a #src; the others need a #value. + if ($type == 'image_button') { + $form[$name]['#src'] = 'misc/druplicon.png'; + } + else { + $form[$name]['#value'] = $name; + } + // 'r' for restricted, so we can test that button click detection code + // correctly takes #access security into account. + if (strpos($arg, 'r') !== FALSE) { + $form[$name]['#access'] = FALSE; + } + } + } + + return $form; +} + +/** + * Form validation handler for the form_test_clicked_button() form. + */ +function form_test_clicked_button_validate($form, &$form_state) { + if (isset($form_state['clicked_button'])) { + drupal_set_message(t('The clicked button is %name.', array('%name' => $form_state['clicked_button']['#name']))); + } + else { + drupal_set_message('There is no clicked button.'); + } +} + +/** + * Form submit handler for the form_test_clicked_button() form. + */ +function form_test_clicked_button_submit($form, &$form_state) { + drupal_set_message('Submit handler for form_test_clicked_button executed.'); +} + /** * Implements hook_form_FORM_ID_alter() for the registration form. diff --git a/modules/system/system.module b/modules/system/system.module index 03952873875887454eda632897aafa04fe5d8ade..a8e57d9241b5b25d9d81718d784922b6e17a2fa7 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -318,6 +318,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'), ); @@ -326,6 +327,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'), ); @@ -333,6 +335,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,