diff --git a/core/modules/views/js/exposed-form-ajax.js b/core/modules/views/js/exposed-form-ajax.js new file mode 100644 index 0000000..a2662c0 --- /dev/null +++ b/core/modules/views/js/exposed-form-ajax.js @@ -0,0 +1,28 @@ +/** + * @file + * Handles Views' exposed form AJAX data submission. + */ + +(function ($) { + 'use strict'; + + /* + * Gets Form build info from settings and adds it to ajax data. + * + * @see views_exposed_form_ajax_enable(). + */ + Drupal.behaviors.ViewsExposedFormAjax = { + attach: function (context, settings) { + Drupal.ajax.instances.forEach(function (instance, key) { + for (var name in drupalSettings.ViewsExposedFormInfo) { + if (drupalSettings.ViewsExposedFormInfo.hasOwnProperty(name)) { + if (instance.options && instance.options.data._triggering_element_name === name) { + jQuery.extend(Drupal.ajax.instances[key].options.data, drupalSettings.ViewsExposedFormInfo[name]); + } + } + }; + }); + } + }; + +})(jQuery); 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 0000000..e0472bc --- /dev/null +++ b/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.info.yml @@ -0,0 +1,8 @@ +name: 'Views Test Exposed Filter' +type: module +description: 'Test module for Views.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - 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 0000000..35ddfc3 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_exposed_filter/views_test_exposed_filter.module @@ -0,0 +1,20 @@ +Default prefix'; + } +} + +function views_test_exposed_filter_ajax_callback(array &$form, FormStateInterface $form_state) { + return [ + '#markup' => 'Callback called.', + ]; +} \ No newline at end of file diff --git a/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php b/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php index 974aa2d..0857cb7 100644 --- a/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php +++ b/core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php @@ -5,6 +5,7 @@ use Drupal\FunctionalJavascriptTests\JavascriptTestBase; use Drupal\simpletest\ContentTypeCreationTrait; use Drupal\simpletest\NodeCreationTrait; +use Drupal\views\Tests\ViewTestData; /** * Tests the basic AJAX functionality of Views exposed forms. @@ -19,7 +20,12 @@ class ExposedFilterAJAXTest extends JavascriptTestBase { /** * {@inheritdoc} */ - public static $modules = ['node', 'views']; + public static $modules = ['node', 'views', 'views_test_config']; + + /** + * {@inheritdoc} + */ + public static $testViews = ['test_content_ajax']; /** * Tests if exposed filtering via AJAX works for the "Content" View. @@ -80,4 +86,37 @@ public function testExposedFiltering() { $this->assertFalse($session->getPage()->hasButton('Reset')); } + /** + * Tests if AJAX events can be attached to the exposed filter form. + */ + public function testExposedFilterAjaxCallback() { + + 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(); + + // Create a user privileged enough to view content. + $user = $this->drupalCreateUser([ + 'administer site configuration', + 'access content', + 'access content overview', + ]); + $this->drupalLogin($user); + + $this->drupalGet('test-content-ajax'); + + $page = $this->getSession()->getPage(); + $this->createScreenshot('/tmp/screen1.jpg'); + $this->assertSession()->pageTextContains('Default prefix'); + $page->fillField('title', 'value'); + $this->createScreenshot('/tmp/screen2.jpg'); + $this->assertSession()->assertWaitOnAjaxRequest(); +$this->createScreenshot('/tmp/screen3.jpg'); + $this->assertSession()->pageTextContains('Callback called.'); + } + } diff --git a/core/modules/views/views.libraries.yml b/core/modules/views/views.libraries.yml index 640492d..8208a44 100644 --- a/core/modules/views/views.libraries.yml +++ b/core/modules/views/views.libraries.yml @@ -16,3 +16,15 @@ views.ajax: - core/jquery.once - core/jquery.form - core/drupal.ajax + +views.exposed-form-ajax: + version: VERSION + js: + js/exposed-form-ajax.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupalSettings + - core/jquery.once + - core/jquery.form + - core/drupal.ajax diff --git a/core/modules/views/views.module b/core/modules/views/views.module index e1a4d8c..3561feb 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -10,6 +10,7 @@ use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\views\Plugin\Derivative\ViewsLocalTask; @@ -639,11 +640,92 @@ function views_pre_render_views_form_views_form($element) { * * Since the exposed form is a GET form, we don't want it to send a wide * variety of information. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state */ function views_form_views_exposed_form_alter(&$form, FormStateInterface $form_state) { $form['form_build_id']['#access'] = FALSE; $form['form_token']['#access'] = FALSE; $form['form_id']['#access'] = FALSE; + // AJAX behaviors need these data, so we add it back in #after_build. + $form['#after_build'][] = 'views_exposed_form_ajax_enable'; +} + +/** + * Add previously removed required form data. + * + * Checks whether the exposed form will use ajax and passes required + * form information removed in views_form_views_exposed_form_alter(). + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * + * @return array + */ +function views_exposed_form_ajax_enable($form, FormStateInterface $form_state) { + // In order for Ajax to work, we need the form build info. Here we + // check if #ajax has been added to any form elements, and if so, + // pass this info as settings via Javascript, which get attached to + // the submitted form on Ajax form submissions. + // #ajax property can be set not only for the first level of the form, + // so we look for it in the whole form. + $ajax_info = []; + $ajax_elements = views_exposed_form_ajax_lookup_recursive($form); + // Determine if form is being processed. + + $triggering_element = $form_state->getTriggeringElement(); + if (!empty($triggering_element['#parents'])) { + $triggering_element_name = end($triggering_element['#parents']); + } + // When we have multiple elements with #ajax set on exposed form + // we need to pass only triggering element name to Javascript. + // Otherwise #ajax will work only for the first element. + if (!empty($triggering_element_name) && !empty($ajax_elements)) { + // Check if element has #ajax property set. + if (in_array($triggering_element_name, $ajax_elements)) { + $ajax_elements = [$triggering_element_name => $triggering_element_name]; + } + else { + $ajax_elements = []; + } + } + if (!empty($ajax_elements)) { + $form_info = [ + 'form_id' => $form['#form_id'], + 'form_build_id' => $form['#build_id'], + ]; + // Anonymous users don't get a token. + if (!empty($form['#token'])) { + $form_info['form_token'] = drupal_get_token($form['#token']); + } + foreach ($ajax_elements as $element_name) { + $ajax_info[$element_name] = $form_info; + } + $form['#attached']['drupalSettings']['ViewsExposedFormInfo'] = $ajax_info; + $form['#attached']['library'][] = 'views/views.exposed-form-ajax'; + } + return $form; +} + +/** + * Recursively looks for the #ajax property for every form elemet. + * @param $elements + * The element array to look for #ajax property. + * + * @return array + * Array of the elements names where #ajax was found. + */ +function views_exposed_form_ajax_lookup_recursive($elements) { + $ajax_elements = []; + foreach (Element::children($elements) as $key) { + if (!empty($elements[$key]['#name']) && !empty($elements[$key]['#ajax'])) { + $ajax_elements[$elements[$key]['#name']] = $elements[$key]['#name']; + } + // Recursive call to look for #ajax in element's childrens. + $ajax_elements += views_exposed_form_ajax_lookup_recursive($elements[$key]); + } + return $ajax_elements; } /**