diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index cd80a18b74532e81fd8d328ca1980a1ddf2079ae..654ebfb065f170c51d2da7530882a8b1bcc24513 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Access\AccessResultInterface; use Drupal\Core\Access\CsrfTokenGenerator; +use Drupal\Core\Ajax\AjaxHelperTrait; use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -30,6 +31,8 @@ */ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface, TrustedCallbackInterface { + use AjaxHelperTrait; + /** * The module handler. * @@ -570,6 +573,22 @@ public function processForm($form_id, &$form, FormStateInterface &$form_state) { $unprocessed_form = $form; $form = $this->doBuildForm($form_id, $form, $form_state); + // If this is being processed as a normal non-AJAX form request in GET mode, + // interrupt the form rendering callback. For example, when using + // HOOK_form_views_exposed_form_alter(). + // Only do this when the form ID matches or it's not specified, since there + // is no guarantee that it's an AJAX request for this particular form. + if ($form_state->isMethodType('get') && !$this->isAjax()) { + $request = $this->requestStack->getCurrentRequest()->request; + $triggering_element_name = $request->get('_triggering_element_name'); + $triggering_element = $form_state->getTriggeringElement(); + if (isset($triggering_element['#name'], $triggering_element['#ajax']) + && (!$request->get('form_id') || $request->get('form_id') == $form_id) + && $triggering_element['#name'] == $triggering_element_name) { + throw new FormAjaxException($form, $form_state); + } + } + // Only process the input if we have a correct form submission. if ($form_state->isProcessingInput()) { // Form values for programmed form submissions typically do not include a @@ -1397,6 +1416,17 @@ protected function getFileUploadMaxSize(): int { return Environment::getUploadMaxSize(); } + /** + * {@inheritdoc} + */ + protected function getRequestWrapperFormat(): string { + $current_request = $this->requestStack->getCurrentRequest(); + if ($current_request) { + return $current_request->get(MainContentViewSubscriber::WRAPPER_FORMAT) ?? ''; + } + return ''; + } + /** * Gets the current active user. * diff --git a/core/modules/media_library/js/media_library.ui.js b/core/modules/media_library/js/media_library.ui.js index 4ff97d57dc1edfabd9d3cd2289454c7b53fc98c2..c7023dc6c765e98c8d720540838ae1b8a3965e5d 100644 --- a/core/modules/media_library/js/media_library.ui.js +++ b/core/modules/media_library/js/media_library.ui.js @@ -320,6 +320,19 @@ enableItems($mediaItems); } } + + function updateModalSelection() { + const mediaLibraryModalSelection = document.querySelector( + '#media-library-modal-selection', + ); + + if (mediaLibraryModalSelection) { + // Set the selection in the hidden form element. + mediaLibraryModalSelection.value = currentSelection.join(); + $(mediaLibraryModalSelection).trigger('change'); + } + } + // Update the selection array and the hidden form field when a media item // is selected. $(once('media-item-change', $mediaItems)).on('change', (e) => { @@ -336,15 +349,7 @@ currentSelection.splice(currentSelection.indexOf(id), 1); } - const mediaLibraryModalSelection = document.querySelector( - '#media-library-modal-selection', - ); - - if (mediaLibraryModalSelection) { - // Set the selection in the hidden form element. - mediaLibraryModalSelection.value = currentSelection.join(); - $(mediaLibraryModalSelection).trigger('change'); - } + updateModalSelection(); // Set the selection in the media library add form. Since the form is // not necessarily loaded within the same context, we can't use the @@ -356,6 +361,7 @@ }); }); checkEnabled(); + updateModalSelection(); // The hidden selection form field changes when the selection is updated. $( once( diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php index 87739a90c96c9381a6b6936abafdcb39336c9e95..70658d6eb0a5d8904e54b79eab844f2f0b0dac85 100644 --- a/core/modules/views/src/ViewExecutable.php +++ b/core/modules/views/src/ViewExecutable.php @@ -745,6 +745,43 @@ public function getExposedInput() { $this->initDisplay(); $this->exposed_input = $this->request->query->all(); + // Allow AJAX requests on exposed filters. + if ($this->request->isMethod('post') && $this->request->request->get('_triggering_element_name')) { + $post_form_data = $this->request->request->all(); + $exposed_field_names = []; + // Go through each handler and let it generate its exposed widget. + foreach ($this->display_handler->handlers as $type => $value) { + /** @var \Drupal\views\Plugin\views\ViewsHandlerInterface $handler */ + foreach ($this->$type as $handler) { + if ($handler->canExpose() && $handler->isExposed()) { + // Pick up POST data for all the exposed handlers. + if (!empty($handler->options['expose']['use_operator']) && !empty($handler->options['expose']['operator_id'])) { + $exposed_field_names[] = $handler->options['expose']['operator_id']; + } + if (!empty($handler->options['expose']['identifier'])) { + if ($handler->isAGroup()) { + $exposed_field_names[] = $handler->options['group_info']['identifier']; + } + else { + $exposed_field_names[] = $handler->options['expose']['identifier']; + } + } + } + } + } + foreach ($exposed_field_names as $exposed_field_name) { + foreach ($post_form_data as $post_form_key => $post_form_value) { + if ($post_form_key === $exposed_field_name || str_starts_with($post_form_key, "{$exposed_field_name}_")) { + // Pick up the exposed field and any extra variations starting + // with the same field name. + $this->exposed_input += [ + $post_form_key => $post_form_value, + ]; + } + } + } + } + // Unset items that are definitely not our input: foreach (['page', 'q'] as $key) { if (isset($this->exposed_input[$key])) { diff --git a/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.info.yml b/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..965b6a8d347bcc59b40a3754e86faf9a96aa79c0 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.info.yml @@ -0,0 +1,7 @@ +name: 'Views Test Exposed Filter' +type: module +description: 'Alters Views exposed filter form for testing AJAX callbacks.' +package: Testing +version: VERSION +dependencies: + - drupal:views diff --git a/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.module b/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.module new file mode 100644 index 0000000000000000000000000000000000000000..ab486d7596762dae0e4846c7c4aa9ee13a279ed1 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.module @@ -0,0 +1,38 @@ +Default prefix'; + } +} + +/** + * Returns render array via an AJAX callback for testing. + * + * @param array $form + * The form definition array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. + * + * @return array + * Render array to display when the AJAX callback is triggered. + */ +function views_test_exposed_filter_ajax_callback(array &$form, FormStateInterface $form_state): array { + return [ + '#markup' => 'Callback called.', + ]; +} diff --git a/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php b/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php index 6d61ecef80a0757ca90b2ddd80de2be5ba70678f..782c03bd694d4e5ff676275b4bdff2c61a48c396 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php @@ -27,6 +27,7 @@ class ExposedFilterAJAXTest extends WebDriverTestBase { 'views', 'views_test_modal', 'user_test_views', + 'views_test_config', ]; /** @@ -39,7 +40,7 @@ class ExposedFilterAJAXTest extends WebDriverTestBase { * * @var array */ - public static $testViews = ['test_user_name']; + public static $testViews = ['test_user_name', 'test_content_ajax']; /** * {@inheritdoc} @@ -258,4 +259,31 @@ public function testExposedFilterErrorMessages(): void { $this->assertSession()->pageTextContainsOnce(sprintf('There are no users matching "%s"', $name)); } + /** + * Tests if Ajax events can be attached to the exposed filter form. + */ + public function testExposedFilterAjaxCallback(): void { + ViewTestData::createTestViews(self::class, ['views_test_config']); + + // Attach an Ajax event to all 'title' fields in the exposed filter form. + \Drupal::service('module_installer')->install(['views_test_exposed_filter']); + $this->resetAll(); + $this->rebuildContainer(); + $this->container->get('module_handler')->reload(); + + $this->drupalGet('test-content-ajax'); + + $page = $this->getSession()->getPage(); + $this->assertSession()->pageTextContains('Default prefix'); + + $page->fillField('title', 'value'); + + // Simulate a click outside the title field so the title field ajax callback + // kicks in. It does not matter here what action is carried out here. + $page->selectFieldOption('status', 'Published'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->assertSession()->pageTextContains('Callback called.'); + } + }