diff --git a/core/lib/Drupal/Core/Render/Element/Checkboxes.php b/core/lib/Drupal/Core/Render/Element/Checkboxes.php index 117f839..a1bd903 100644 --- a/core/lib/Drupal/Core/Render/Element/Checkboxes.php +++ b/core/lib/Drupal/Core/Render/Element/Checkboxes.php @@ -127,4 +127,35 @@ public static function valueCallback(&$element, $input, FormStateInterface $form } } + /** + * Determines which checkboxes were checked when a form is submitted. + * + * @param array $input + * An array returned by the FormAPI for a set of checkboxes. + * + * @return array + * An array of keys that were checked. + */ + static public function getCheckedCheckboxes(array $input) { + // Checkboxes show up as an array in the form of option_id => option_id|0. + // If all the values are zero, then there is no input. + $checked = array_filter($input, function($value) { + return $value !== 0; + }); + return array_keys($checked); + } + + /** + * Determines if the submitted checkbox values include a selected option. + * + * @param array $input + * Form values returned from a set of checkboxes. + * + * @return bool + * TRUE if all options are unchecked. FALSE otherwise. + */ + static public function detectEmptyCheckboxes(array $input) { + return (bool) empty(static::getCheckedCheckboxes($input)); + } + } diff --git a/core/modules/views/src/Form/ViewsExposedForm.php b/core/modules/views/src/Form/ViewsExposedForm.php index e2b91a5..a6f6987 100644 --- a/core/modules/views/src/Form/ViewsExposedForm.php +++ b/core/modules/views/src/Form/ViewsExposedForm.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\Html; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\Checkboxes; use Drupal\Core\Url; use Drupal\views\ExposedFormCache; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -151,26 +152,43 @@ public function validateForm(array &$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { + // Form input keys that will not be included in $view->exposed_raw_data. + $exclude = array('submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', 'reset'); + $values = $form_state->getValues(); foreach (array('field', 'filter') as $type) { /** @var \Drupal\views\Plugin\views\ViewsHandlerInterface[] $handlers */ $handlers = &$form_state->get('view')->$type; foreach ($handlers as $key => $info) { - $handlers[$key]->submitExposed($form, $form_state); + if ($handlers[$key]->acceptExposedInput($values)) { + $handlers[$key]->submitExposed($form, $form_state); + } + else { + // The input from the form did not validate, exclude it from the + // stored raw data. + $exclude[] = $key; + } } } $view = $form_state->get('view'); - $view->exposed_data = $form_state->getValues(); + $view->exposed_data = $values; $view->exposed_raw_input = []; - $exclude = array('submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', 'reset'); /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */ $exposed_form_plugin = $view->display_handler->getPlugin('exposed_form'); $exposed_form_plugin->exposedFormSubmit($form, $form_state, $exclude); - - foreach ($form_state->getValues() as $key => $value) { - if (!in_array($key, $exclude)) { - $view->exposed_raw_input[$key] = $value; + foreach ($values as $key => $value) { + if (!empty($key) && !in_array($key, $exclude)) { + if (is_array($value)) { + // Handle checkboxes, we only want to include the checked options. + $checked = Checkboxes::getCheckedCheckboxes($value); + foreach ($checked as $option_id) { + $view->exposed_raw_input[$option_id] = $value[$option_id]; + } + } + else { + $view->exposed_raw_input[$key] = $value; + } } } } diff --git a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php index c0cd5d0..b55df9f 100644 --- a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php +++ b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php @@ -12,6 +12,7 @@ use Drupal\Core\Form\FormHelper; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Render\Element\Checkboxes; use Drupal\user\RoleInterface; use Drupal\views\Plugin\views\HandlerBase; use Drupal\Component\Utility\Html; @@ -1320,15 +1321,20 @@ public function storeGroupInput($input, $status) { } /** - * Check to see if input from the exposed filters should change - * the behavior of this filter. + * Determines if the input from a filter should change the generated query. + * + * @param array $input + * The exposed data for this view. + * + * @return bool + * TRUE if the input for this filter should be included in the view query. + * FALSE otherwise. */ public function acceptExposedInput($input) { if (empty($this->options['exposed'])) { return TRUE; } - if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) { $this->operator = $input[$this->options['expose']['operator_id']]; } @@ -1346,6 +1352,12 @@ public function acceptExposedInput($input) { if ($value == 'All' || $value === array()) { return FALSE; } + + // If checkboxes are used to render this filter, we do not include the + // filter if all option are unchecked. + if (is_array($value) && Checkboxes::detectEmptyCheckboxes($value)) { + return FALSE; + } } if (!empty($this->alwaysMultiple) && $value === '') { diff --git a/core/modules/views/src/Plugin/views/filter/InOperator.php b/core/modules/views/src/Plugin/views/filter/InOperator.php index 042dca5..4d7bac0 100644 --- a/core/modules/views/src/Plugin/views/filter/InOperator.php +++ b/core/modules/views/src/Plugin/views/filter/InOperator.php @@ -287,15 +287,17 @@ public function reduceValueOptions($input = NULL) { return $options; } + /** + * @inheritdoc + */ public function acceptExposedInput($input) { - // A very special override because the All state for this type of - // filter could have a default: if (empty($this->options['exposed'])) { return TRUE; } - // If this is non-multiple and non-required, then this filter will - // participate, but using the default settings, *if* 'limit is true. + // The "All" state for this type of filter could have a default value. If + // this is a non-multiple and non-required option, then this filter will + // participate, but using the default settings *if* 'limit' is true. if (empty($this->options['expose']['multiple']) && empty($this->options['expose']['required']) && !empty($this->options['expose']['limit'])) { $identifier = $this->options['expose']['identifier']; if ($input[$identifier] == 'All') { diff --git a/core/modules/views/src/Tests/Plugin/ExposedFormTest.php b/core/modules/views/src/Tests/Plugin/ExposedFormTest.php index 1c520e0..50d040e 100644 --- a/core/modules/views/src/Tests/Plugin/ExposedFormTest.php +++ b/core/modules/views/src/Tests/Plugin/ExposedFormTest.php @@ -148,6 +148,48 @@ public function testExposedFormRender() { } /** + * Tests overriding the default render option with checkboxes. + */ + public function testExposedFormRenderCheckboxes() { + // Make sure we have at least two options for node type. + $this->drupalCreateContentType(['type' => 'page']); + $this->drupalCreateNode(['type' => 'page']); + + // Use a test theme to convert multi-select elements into checkboxes. + \Drupal::service('theme_handler')->install(array('views_test_checkboxes_theme')); + $this->config('system.theme') + ->set('default', 'views_test_checkboxes_theme') + ->save(); + + // Set the "type" filter to multi-select. + $view = Views::getView('test_exposed_form_buttons'); + $filter = $view->getHandler('page_1', 'filter', 'type'); + $filter['expose']['multiple'] = TRUE; + $view->setHandler('page_1', 'filter', 'type', $filter); + + // Only display 5 items per page so we can test that paging works. + $display = &$view->storage->getDisplay('default'); + $display['display_options']['pager']['options']['items_per_page'] = 5; + + $view->save(); + $this->drupalGet('test_exposed_form_buttons'); + + $actual = $this->xpath('//form//input[@type="checkbox" and @name="type[article]"]'); + $this->assertEqual(count($actual), 1, 'Article option renders as a checkbox.'); + $actual = $this->xpath('//form//input[@type="checkbox" and @name="type[page]"]'); + $this->assertEqual(count($actual), 1, 'Page option renders as a checkbox'); + + // Ensure that all results are displayed. + $rows = $this->xpath("//div[contains(@class, 'views-row')]"); + $this->assertEqual(count($rows), 5, '5 rows are displayed by default on the first page when no options are checked.'); + + $this->clickLink('Page 2'); + $rows = $this->xpath("//div[contains(@class, 'views-row')]"); + $this->assertEqual(count($rows), 1, '1 row is displayed by default on the second page when no options are checked.'); + $this->assertNoText('An illegal choice has been detected. Please contact the site administrator.'); + } + + /** * Tests the exposed block functionality. */ public function testExposedBlock() { diff --git a/core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.info.yml b/core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.info.yml new file mode 100644 index 0000000..50bf73e --- /dev/null +++ b/core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.info.yml @@ -0,0 +1,5 @@ +name: Views test checkboxes theme +type: theme +description: Theme for testing Views rendering of checkboxes. +version: VERSION +core: 8.x diff --git a/core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.theme b/core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.theme new file mode 100644 index 0000000..4a038ae --- /dev/null +++ b/core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.theme @@ -0,0 +1,7 @@ +