Commit c02c02bb by Manzar Hussain

new plugin

parent b68ffadc
defaults: &defaults
docker:
# specify the version you desire here (avoid latest except for testing)
- image: mkeereman/drupal8_tests:8.7
- image: selenium/standalone-chrome-debug:3.14.0-beryllium
- image: mariadb:10.3
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 1
working_directory: /var/www/html/modules/better_exposed_filters
# YAML does not support merging of lists. That means we can't have a default
# 'steps' configuration, though we can have defaults for individual step
# properties.
# We use the composer.json as a way to determine if we can cache our build.
restore_cache: &restore_cache
keys:
- v4-dependencies-{{ checksum "composer.json" }}-{{ checksum "../../composer.json" }}
# If composer.json hasn't changed, restore the Composer cache directory. We
# don't restore the lock file so we ensure we get updated dependencies.
save_cache: &save_cache
paths:
- /root/.composer/cache/files
key: v4-dependencies-{{ checksum "composer.json" }}-{{ checksum "../../composer.json" }}
# Install composer dependencies into the workspace to share with all jobs.
update_dependencies: &update_dependencies
<<: *defaults
steps:
- checkout
- restore_cache: *restore_cache
- run:
working_directory: /var/www/html
command: |
./update-dependencies.sh $CIRCLE_PROJECT_REPONAME
- save_cache: *save_cache
- persist_to_workspace:
root: /var/www/html
paths:
- .
# Run Drupal unit and kernel tests as one job. This command invokes the test.sh
# hook.
unit_kernel_tests: &unit_kernel_tests
<<: *defaults
steps:
- attach_workspace:
at: /var/www/html
- checkout
- run:
working_directory: /var/www/html
command: |
./test.sh $CIRCLE_PROJECT_REPONAME
- store_test_results:
path: /var/www/html/artifacts/phpunit
- store_artifacts:
path: /var/www/html/artifacts
# Run Drupal functional and functional JS tests as one job. This command invokes
# the test-functional.sh and test-functional-js hooks.
functional_tests: &functional_tests
<<: *defaults
steps:
- attach_workspace:
at: /var/www/html
- checkout
- run:
working_directory: /var/www/html
command: |
./test-functional.sh $CIRCLE_PROJECT_REPONAME
./test-functional-js.sh $CIRCLE_PROJECT_REPONAME
- store_test_results:
path: /var/www/html/artifacts/phpunit
- store_artifacts:
path: /var/www/html/artifacts
# Run code quality tests. This invokes code-sniffer.sh.
code_sniffer: &code_sniffer
<<: *defaults
steps:
- attach_workspace:
at: /var/www/html
- checkout
# Don't exit builds in CI when warning/error occurs.
# @todo fix errors so this becomes reliable
- run:
working_directory: /var/www/html
command: |
vendor/bin/phpcs --config-set ignore_warnings_on_exit 1
vendor/bin/phpcs --config-set ignore_errors_on_exit 1
./code-sniffer.sh $CIRCLE_PROJECT_REPONAME
- store_test_results:
path: /var/www/html/artifacts/phpcs
- store_artifacts:
path: /var/www/html/artifacts
# Run code coverage tests. This invokes code-coverage-stats.sh.
code_coverage: &code_coverage
<<: *defaults
steps:
- attach_workspace:
at: /var/www/html
- checkout
- run:
working_directory: /var/www/html
command: |
./code-coverage-stats.sh $CIRCLE_PROJECT_REPONAME
- store_artifacts:
path: /var/www/html/artifacts
# Declare all of the jobs we should run.
version: 2
jobs:
update-dependencies:
<<: *update_dependencies
run-unit-kernel-tests:
<<: *unit_kernel_tests
run-functional-tests:
<<: *functional_tests
run-code-sniffer:
<<: *code_sniffer
run-code-coverage:
<<: *code_coverage
workflows:
version: 2
# Declare a workflow that runs all of our jobs in parallel.
test_and_lint:
jobs:
- update-dependencies
- run-unit-kernel-tests:
requires:
- update-dependencies
- run-functional-tests:
requires:
- update-dependencies
- run-code-sniffer:
requires:
- update-dependencies
- run-code-coverage:
requires:
- update-dependencies
- run-unit-kernel-tests
# OS generated files #
######################
.DS_Store*
ehthumbs.db
Icon
Thumbs.db
._*
vendor
composer.lock
.idea
-- SUMMARY --
The Better Exposed Filters module replaces the Views' default single- or
multi-select boxes with more advanced options such as radio buttons, checkboxes,
toggle links or jQueryUI widgets.
Views Filters are a powerful tool to limit the results of a given view. When you
expose a filter, you allow the user to interact with the view making it easy to
build a customized advanced search. For example, exposing a taxonomy filter
lets your site visitor search for articles with specific tags. Better Exposed
Filters gives you greater control over the rendering of exposed filters.
For a full description of the module, visit the project page:
https://drupal.org/project/better_exposed_filters
Documentation:
https://www.drupal.org/node/766974
To submit bug reports and feature suggestions, or to track changes:
https://drupal.org/project/issues/better_exposed_filters
-- INSTALLATION --
Install as usual, see http://drupal.org/node/70151 for further instructions.
-- CONFIGURATION --
See the documentation on Drupal.org.
-- CONTACT --
The maintainer for this project is Mike Keran (https://www.drupal.org/u/mikeker)
He can be contacted through his personal web site (http://MikeKeran.com) for
work on this module or other custom projects.
<?php
/**
* @file
* Hooks provided by the Better Exposed Filters module.
*/
/**
* Alters BEF options before the exposed form widgets are built.
*
* @param array $options
* The BEF options array.
* @param \Drupal\views\ViewExecutable $view
* The view to which the settings apply.
* @param \Drupal\views\Plugin\views\display\DisplayPluginBase $displayHandler
* The display handler to which the settings apply.
*/
function hook_better_exposed_filters_options_alter(array &$options, ViewExecutable $view, DisplayPluginBase $displayHandler) {
// Set the min/max value of a slider.
$settings['field_price_value']['slider_options']['bef_slider_min'] = 500;
$settings['field_price_value']['slider_options']['bef_slider_max'] = 5000;
}
/**
* Modify the array of BEF display options for an exposed filter.
*
* @param array $widgets
* The set of BEF widgets available to this filter.
* @param \Drupal\views\Plugin\views\HandlerBase $filter
* The exposed views filter plugin.
*/
function hook_better_exposed_filters_filter_widgets_alter(array &$widgets, HandlerBase $filter) {
if ($filter instanceof CustomViewsFilterFoo) {
$widgets['bef_links'] = t('Links');
}
}
name: Better Exposed Filters
description: Provides advanced options (e.g. links, checkboxes, or other widgets) to exposed Views elements.
core_version_requirement: ^8.8 || ^9
type: module
package: Views
dependencies:
- drupal:views
- jquery_ui:jquery_ui
- jquery_ui_slider:jquery_ui_slider
- jquery_ui_touch_punch:jquery_ui_touch_punch
- jquery_ui_datepicker:jquery_ui_datepicker
# Information added by Drupal.org packaging script on 2020-07-07
version: '8.x-5.0-beta1'
project: 'better_exposed_filters'
datestamp: 1594141894
<?php
/**
* @file
* Contains better_exposed_filters.install.
*/
/**
* Provide upgrade path from 8.x-3.x to 8.x-4.x.
*/
function better_exposed_filters_update_8001() {
$config_factory = \Drupal::configFactory();
foreach ($config_factory->listAll('views.view.') as $config_name) {
$config = $config_factory->getEditable($config_name);
// Go through each display on each view.
$displays = $config->get('display');
foreach ($displays as $display_name => $display) {
if (!empty($display['display_options']['exposed_form'])) {
$exposed_form = $display['display_options']['exposed_form'];
// Find BEF exposed forms.
if (isset($exposed_form['type']) && $exposed_form['type'] === 'bef') {
$bef_settings = [];
foreach ($exposed_form['options']['bef'] as $type => $option) {
// General settings.
if ($type === 'general') {
$bef_settings['general'] = [
'autosubmit' => $option['autosubmit'] ?? FALSE,
'autosubmit_exclude_textfield' => $option['autosubmit_exclude_textfield'] ?? FALSE,
'autosubmit_hide' => $option['autosubmit_hide'] ?? FALSE,
'input_required' => $exposed_form['options']['input_required'] ?? FALSE,
];
if (isset($exposed_form['options']['text_input_required'])) {
$bef_settings['general'] += [
'text_input_required' => $exposed_form['options']['text_input_required'],
'text_input_required_format' => 'basic_html',
];
}
$bef_settings['general'] += [
'allow_secondary' => $option['allow_secondary'] ?? FALSE,
'secondary_label' => $option['secondary_label'] ?? 'Advanced options',
];
}
// Sort settings.
elseif ($type === 'sort') {
$bef_settings['sort'] = [
'plugin_id' => $option['bef_format'] ?? 'default',
'advanced' => [
'combine' => $option['advanced']['combine'] ?? FALSE,
'combine_rewrite' => $option['advanced']['combine_rewrite'] ?? '',
'reset' => $option['advanced']['reset'] ?? FALSE,
'reset_label' => $option['advanced']['reset_label'] ?? '',
'collapsible' => $option['advanced']['collapsible'] ?? FALSE,
'collapsible_label' => $option['advanced']['collapsible_label'] ?? '',
'is_secondary' => $option['advanced']['is_secondary'] ?? FALSE,
],
];
}
// Pager settings.
elseif ($type === 'pager') {
$bef_settings['pager'] = [
'plugin_id' => $option['bef_format'] ?? 'default',
'advanced' => [
'is_secondary' => $option['is_secondary'] ?? FALSE,
],
];
}
// Filter settings.
else {
// This would indicate a newer version of the config already.
if ($type === 'filter') {
continue;
}
$field_name = $type;
$bef_settings['filter'][$field_name] = [
'plugin_id' => $option['bef_format'] ?? 'default',
];
// Checkboxes/Radio buttons.
if ($option['bef_format'] === 'bef') {
$bef_settings['filter'][$field_name]['select_all_none'] = $option['more_options']['select_all_none'] ?? FALSE;
$bef_settings['filter'][$field_name]['select_all_none_nested'] = $option['more_options']['select_all_none_nested'] ?? FALSE;
}
// Links.
if ($option['bef_format'] === 'bef_links') {
$bef_settings['filter'][$field_name]['select_all_none'] = $option['more_options']['select_all_none'] ?? FALSE;
}
// Sliders.
if ($option['bef_format'] === 'bef_sliders') {
// Animate option is now split into two separate options.
$animate = $option['slider_options']['bef_slider_animate'];
$animate_ms = 0;
if (empty($animate)) {
$animate = 'none';
}
elseif (is_int($animate)) {
$animate = 'custom';
$animate_ms = $animate;
}
$bef_settings['filter'][$field_name] = array_merge($bef_settings['filter'][$field_name], [
'min' => $option['slider_options']['bef_slider_min'] ?? 0,
'max' => $option['slider_options']['bef_slider_max'] ?? 99999,
'step' => $option['slider_options']['bef_slider_step'] ?? 1,
'animate' => $animate,
'animate_ms' => $animate_ms,
'orientation' => $option['slider_options']['bef_slider_orientation'] ?? 'horizontal',
]);
}
// Shared advanced settings.
$bef_settings['filter'][$field_name]['advanced'] = [
'collapsible' => $option['more_options']['bef_collapsible'] ?? FALSE,
'is_secondary' => $option['more_options']['is_secondary'] ?? FALSE,
'placeholder_text' => $option['more_options']['placeholder_text'] ?? '',
'rewrite' => [
'filter_rewrite_values' => $option['more_options']['rewrite']['filter_rewrite_values'] ?? '',
],
// New option.
'sort_options' => FALSE,
];
}
}
// Update BEF settings.
$config->set("display.$display_name.display_options.exposed_form.options.bef", $bef_settings);
}
}
}
$config->save(TRUE);
}
}
general:
version: 4.x
css:
theme:
css/better_exposed_filters.css: {}
js:
js/better_exposed_filters.js: {}
dependencies:
- core/drupal
- core/jquery
auto_submit:
version: 4.x
js:
js/auto_submit.js: {}
dependencies:
- core/drupal
- core/jquery.once
- core/drupal.debounce
select_all_none:
version: 4.x
js:
js/bef_select_all_none.js: {}
dependencies:
- core/drupal
- core/jquery
- core/jquery.once
sliders:
version: 4.x
js:
js/bef_sliders.js: {}
dependencies:
- core/drupal
- core/jquery
- core/jquery.once
- jquery_ui/mouse
- jquery_ui_touch_punch/touch-punch
- jquery_ui_slider/slider
datepickers:
version: 4.x
js:
js/bef_datepickers.js: {}
dependencies:
- core/drupal
- core/jquery
- jquery_ui/mouse
- jquery_ui_touch_punch/touch-punch
- jquery_ui_datepicker/datepicker
<?php
/**
* @file
* General functions and hook implementations.
*
* @see https://www.drupal.org/project/better_exposed_filters
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
require_once __DIR__ . '/includes/better_exposed_filters.theme.inc';
/**
* Implements hook_help().
*/
function better_exposed_filters_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the better_exposed_filters module.
case 'help.page.better_exposed_filters':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Better Exposed Filters (BEF) modifies the use of Views by replacing the \'single\' or \'multi\' <em>select boxes</em> with <em>radio buttons or checkboxes</em>. Views offers the ability to expose filters to the end user. When you expose a filter, you allow the user to interact with the view making it easy to build an advanced search. Better Exposed Filters gives you greater control over the rendering of exposed filters. For more information, see the <a href=":online">online documentation for the Better Exposed Filters module</a>.', [':online' => 'https://www.drupal.org/node/766974']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dt>' . t('Editing or Creating Views') . '</dt>';
$output .= '<dd>' . t('Better Exposed Filters is used on <a href=":views">Views</a> that use an exposed filter. Views filters are used to reduce the result set of a View to a manageable amount of data. BEF only operates on fields that have a limited number of options such as <a href=":node">Node</a>:Type or <a href=":taxonomy">Taxonomy</a>:TermID.', [':views' => Url::fromRoute('help.page', ['name' => 'views'])->toString(), ':node' => Url::fromRoute('help.page', ['name' => 'node'])->toString(), ':taxonomy' => (\Drupal::moduleHandler()->moduleExists('taxonomy')) ? Url::fromRoute('help.page', ['name' => 'taxonomy'])->toString() : '#']) . '</dd>';
$output .= '<dt>' . t('Styling Better Exposed Filters') . '</dt>';
$output .= '<dd>' . t('BEF provides some additional HTML structure to help you style your exposed filters. For some common examples see the <a href=":doco">online documentation</a>.', [':doco' => 'https://www.drupal.org/node/766974']) . '</dd>';
return $output;
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function better_exposed_filters_form_views_ui_config_item_form_alter(&$form, FormStateInterface $form_state) {
// Checks if Token module is enabled.
if (!\Drupal::moduleHandler()->moduleExists('token')) {
$text = t('Enable the Token module to allow token replacement in this field.');
if (empty($form['options']['expose']['description']['#description'])) {
$form['options']['expose']['description']['#description'] = $text;
}
else {
$form['options']['expose']['description']['#description'] .= " $text";
}
return;
}
// Adds global token replacements, if available.
$text = t('Tokens are allowed in this field. Replacement options can be found in the "Global replacement patterns" section, below.');
if (empty($form['options']['expose']['description']['#description'])) {
$form['options']['expose']['description']['#description'] = $text;
}
else {
$form['options']['expose']['description']['#description'] .= " $text";
}
$form['options']['expose']['global_replacement_tokens'] = [
'#title' => t('Global replacement patterns (for description field only)'),
'#type' => 'details',
'#weight' => 151,
];
$form['options']['expose']['global_replacement_tokens']['list'] = [
'#theme' => 'token_tree_link',
'#token_types' => [],
];
}
services:
# Helpers
better_exposed_filters.bef_helper:
class: Drupal\better_exposed_filters\BetterExposedFiltersHelper
# Plugins
plugin.manager.better_exposed_filters_filter_widget:
class: Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager
arguments: [filter, '@container.namespaces', '@cache.discovery', '@module_handler']
plugin.manager.better_exposed_filters_pager_widget:
class: Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager
arguments: [pager, '@container.namespaces', '@cache.discovery', '@module_handler']
plugin.manager.better_exposed_filters_sort_widget:
class: Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager
arguments: [sort, '@container.namespaces', '@cache.discovery', '@module_handler']
{
"name": "drupal/better_exposed_filters",
"type": "drupal-module",
"description": "Replaces the Views default single- or multi-select boxes with more advanced options.",
"homepage": "https://www.drupal.org/project/better_exposed_filters",
"authors": [
{
"name": "Mike Keran",
"homepage": "https://www.drupal.org/u/mikeker"
},
{
"name": "Martin Keereman",
"homepage": "https://www.drupal.org/u/etroid"
}
],
"support": {
"issues": "https://www.drupal.org/project/issues/better_exposed_filters",
"source": "https://git.drupalcode.org/project/better_exposed_filters"
},
"license": "GPL-2.0+",
"repositories": [
{
"type": "composer",
"url": "https://packages.drupal.org/8"
}
],
"require": {
"drupal/jquery_ui": "^1.4",
"drupal/jquery_ui_slider": "^1.1",
"drupal/jquery_ui_touch_punch": "^1.0",
"drupal/jquery_ui_datepicker": "^1.0"
}
}
#
# Schema for the Better Exposed Filters configuration files.
#
# Views exposed form.
views.exposed_form.bef:
type: views_exposed_form
label: 'Better Exposed Filters'
mapping:
input_required:
type: boolean
label: 'Input required before showing results'
text_input_required:
type: text
label: 'Text shown if a filter option has not been selected'
text_input_required_format:
type: text
label: 'Text format for the text_input_required field'
bef:
type: mapping
label: 'BEF options'
mapping:
general:
type: better_exposed_filters.general
sort:
type: better_exposed_filters.sort.[plugin_id]
pager:
type: better_exposed_filters.pager.[plugin_id]
filter:
type: sequence
label: 'Filters'
sequence:
type: better_exposed_filters.filter.[plugin_id]
#
# BEF general settings.
#
better_exposed_filters.general:
label: 'General BEF settings'
type: mapping
mapping:
autosubmit:
type: boolean
label: 'Auto-submit'
autosubmit_exclude_textfield:
type: boolean
label: 'Exclude Textfield'
autosubmit_textfield_delay:
type: integer
label: 'Delay for textfield autosubmit'
autosubmit_hide:
type: boolean
label: 'Hide submit button'
input_required:
type: boolean
label: 'Only display results after the user has selected a filter option'
allow_secondary:
type: boolean
label: 'Enable secondary exposed form options'
secondary_label:
type: label
label: 'Secondary options label'
secondary_open:
type: boolean
label: 'Secondary options is open'
#
# Schema for the Better Exposed Filters filter widgets.
#
better_exposed_filters_filter_widget:
type: mapping
mapping:
plugin_id:
type: string
label: 'Plugin id'
advanced:
type: mapping
mapping:
sort_options:
type: boolean
label: 'Sort filter options alphabetically'
placeholder_text:
type: string
label: 'Placeholder text for the filter'
rewrite:
type: mapping
label: 'Rewrite filter options'
mapping:
filter_rewrite_values:
type: text
label: 'Rewrite filter option'
collapsible:
type: boolean
label: 'Make sort options collapsible'
is_secondary:
type: boolean
label: 'This filter is a secondary option'
# BEF exposed filter widgets
better_exposed_filters.filter.*:
label: 'Exposed filter BEF settings'
type: better_exposed_filters_filter_widget
better_exposed_filters.filter.default:
label: 'Default'
type: better_exposed_filters_filter_widget
better_exposed_filters.filter.bef:
label: 'Checkboxes/Radio Buttons'
type: better_exposed_filters_filter_widget
mapping:
select_all_none:
type: boolean
label: 'Add select all/none links'
select_all_none_nested:
type: boolean
label: 'Add select all/none links to hierarchical lists'
better_exposed_filters.filter.bef_links:
label: 'Links'
type: better_exposed_filters_filter_widget
mapping:
select_all_none:
type: boolean
label: 'Add select all/none links'
better_exposed_filters.filter.bef_hidden:
label: 'Hidden'
type: better_exposed_filters_filter_widget
better_exposed_filters.filter.bef_sliders:
label: 'jQuery UI Slider'
type: better_exposed_filters_filter_widget
mapping:
min:
type: string
label: 'Range minimum'
max:
type: string
label: 'Range maximum'
step:
type: string
label: 'Step'
animate:
type: string
label: 'Animation speed'
animate_ms:
type: string
label: 'Animation speed in milliseconds'
orientation:
type: string
label: 'Orientation'
better_exposed_filters.filter.bef_datepickers:
label: 'jQuery UI Date Picker'
type: better_exposed_filters_filter_widget
#
# Schema for the Better Exposed Filters pager widgets.
#
better_exposed_filters_pager_widget:
type: mapping
mapping:
plugin_id:
type: string
label: 'Plugin id'
advanced:
type: mapping
mapping:
is_secondary:
type: boolean
label: 'Pager is a secondary option'
# BEF exposed pager widgets
better_exposed_filters.pager.*:
type: better_exposed_filters_pager_widget
label: 'Exposed pager BEF settings'
better_exposed_filters.pager.default:
type: better_exposed_filters_pager_widget
label: 'Default'
better_exposed_filters.pager.bef:
type: better_exposed_filters_pager_widget
label: 'Checkboxes/Radio Buttons'
better_exposed_filters.pager.bef_links:
type: better_exposed_filters_pager_widget
label: 'Links'
#
# Schema for the Better Exposed Filters sort widgets.
#
better_exposed_filters_sort_widget:
type: mapping
mapping:
plugin_id:
type: string
label: 'Plugin id'
advanced:
type: mapping
mapping:
combine:
type: boolean
label: 'Combine sort order with sort by'
combine_rewrite:
type: text
label: 'Rewrite combined sort options'
reset:
type: boolean
label: 'Include reset sort option'
reset_label:
type: label
label: 'Reset sort label'
collapsible:
type: boolean
label: 'Make sort options collapsible'
collapsible_label:
type: label
label: 'Collapsible details element title'
is_secondary:
type: boolean
label: 'Sort is a secondary option'
# BEF exposed sort widgets
better_exposed_filters.sort.*:
type: better_exposed_filters_sort_widget
label: 'Exposed sort BEF settings'
better_exposed_filters.sort.default:
type: better_exposed_filters_sort_widget
label: 'Default'
better_exposed_filters.sort.bef:
type: better_exposed_filters_sort_widget
label: 'Checkboxes/Radio Buttons'
better_exposed_filters.sort.bef_links:
type: better_exposed_filters_sort_widget
label: 'Links'
/**
* @file
* better_exposed_filters.css
*/
/*
* Basic styling for features added by Better Exposed Filters
*/
.bef-exposed-form .form--inline .form-item {
float: none;
}
.bef-exposed-form .form--inline > .form-item {
float: left; /* LRT */
}
.bef-exposed-form .form--inline .bef--secondary {
clear: left;
}
version: "3"
services:
drupal8:
image: mkeereman/drupal8_tests:8.7
ports:
- "8081:80"
volumes:
- ./:/app
networks:
bef:
mariadb-host:
image: mariadb:10.3
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 1
ports:
- "3309:3306"
networks:
bef:
chrome:
image: selenium/standalone-chrome-debug:3.14.0-beryllium
ports:
- "4444:4444"
- "32771:5900"
networks:
bef:
networks:
bef:
driver: bridge
/**
* @file
* auto_submit.js
*
* Provides a "form auto-submit" feature for the Better Exposed Filters module.
*/
(function ($, Drupal) {
/**
* To make a form auto submit, all you have to do is 3 things:.
*
* Use the "better_exposed_filters/auto_submit" js library.
*
* On gadgets you want to auto-submit when changed, add the
* data-bef-auto-submit attribute. With FAPI, add:
* @code
* '#attributes' => array('data-bef-auto-submit' => ''),
* @endcode
*
* If you want to have auto-submit for every form element, add the
* data-bef-auto-submit-full-form to the form. With FAPI, add:
* @code
* '#attributes' => array('data-bef-auto-submit-full-form' => ''),
* @endcode
*
* If you want to exclude a field from the bef-auto-submit-full-form auto
* submission, add an attribute of data-bef-auto-submit-exclude to the form
* element. With FAPI, add:
* @code
* '#attributes' => array('data-bef-auto-submit-exclude' => ''),
* @endcode
*
* Finally, you have to identify which button you want clicked for autosubmit.
* The behavior of this button will be honored if it's ajaxy or not:
* @code
* '#attributes' => array('data-bef-auto-submit-click' => ''),
* @endcode
*
* Currently only 'select', 'radio', 'checkbox' and 'textfield' types are
* supported. We probably could use additional support for HTML5 input types.
*/
Drupal.behaviors.betterExposedFiltersAutoSubmit = {
attach: function (context) {
// When exposed as a block, the form #attributes are moved from the form
// to the block element, thus the second selector.
// @see \Drupal\block\BlockViewBuilder::preRender
var selectors = 'form[data-bef-auto-submit-full-form], [data-bef-auto-submit-full-form] form, [data-bef-auto-submit]';
// The change event bubbles so we only need to bind it to the outer form
// in case of a full form, or a single element when specified explicitly.
$(selectors, context).addBack(selectors).each(function (i, e) {
// Store the current form.
var $form = $(e);
// Retrieve the autosubmit delay for this particular form.
var autoSubmitDelay = $form.data('bef-auto-submit-delay') || 500;
// Attach event listeners.
$form.once('bef-auto-submit')
// On change, trigger the submit immediately.
.on('change', triggerSubmit)
// On keyup, wait for a specified number of milliseconds before
// triggering autosubmit. Each new keyup event resets the timer.
.on('keyup', Drupal.debounce(triggerSubmit, autoSubmitDelay));
});
/**
* Triggers form autosubmit when conditions are right.
*
* - Checks first that the element that was the target of the triggering
* event is `:text` or `textarea`, but is not `.hasDatePicker`.
* - Checks that the keycode of the keyup was not in the list of ignored
* keys (navigation keys etc).
*
* @param {object} e - The triggering event.
*/
function triggerSubmit(e) {
// e.keyCode: key.
var ignoredKeyCodes = [
16, // Shift.
17, // Ctrl.
18, // Alt.
20, // Caps lock.
33, // Page up.
34, // Page down.
35, // End.
36, // Home.
37, // Left arrow.
38, // Up arrow.
39, // Right arrow.
40, // Down arrow.
9, // Tab.
13, // Enter.
27 // Esc.
];
// Triggering element.
var $target = $(e.target);
var $submit = $target.closest('form').find('[data-bef-auto-submit-click]');
// Don't submit on changes to excluded elements or a submit element.
if ($target.is('[data-bef-auto-submit-exclude], :submit')) {
return true;
}
// Submit only if this is a non-datepicker textfield and if the
// incoming keycode is not one of the excluded values.
if (
$target.is(':text:not(.hasDatepicker), textarea')
&& $.inArray(e.keyCode, ignoredKeyCodes) === -1
) {
$submit.click();
}
// Only trigger submit if a change was the trigger (no keyup).
else if (e.type === 'change') {
$submit.click();
}
}
}
}
}(jQuery, Drupal));
/**
* @file
* bef_datepickers.js
*
* Provides jQueryUI Datepicker integration with Better Exposed Filters.
*/
(function ($, Drupal, drupalSettings) {
/*
* Helper functions
*/
Drupal.behaviors.betterExposedFiltersDatePickers = {
attach: function (context, settings) {
// Check for and initialize datepickers.
var befSettings = drupalSettings.better_exposed_filters;
if (befSettings && befSettings.datepicker && befSettings.datepicker_options && $.fn.datepicker) {
var opt = [];
$.each(befSettings.datepicker_options, function (key, val) {
if (key && val) {
opt[key] = JSON.parse(val);
}
});
$('.bef-datepicker').datepicker(opt);
}
}
};
})(jQuery, Drupal, drupalSettings);
/**
* @file
* bef_select_all_none.js
*
* Adds select all/none toggle functionality to an exposed filter.
*/
(function ($) {
Drupal.behaviors.betterExposedFiltersSelectAllNone = {
attach: function (context) {
/*
* Add Select all/none links to specified checkboxes
*/
var selected = $('.form-checkboxes.bef-select-all-none:not(.bef-processed)');
if (selected.length) {
var selAll = Drupal.t('Select All');
var selNone = Drupal.t('Select None');
// Set up a prototype link and event handlers.
var link = $('<a class="bef-toggle" href="#">' + selAll + '</a>')
link.click(function (event) {
// Don't actually follow the link...
event.preventDefault();
event.stopPropagation();
if (selAll == $(this).text()) {
// Select all the checkboxes.
$(this)
.html(selNone)
.siblings('.bef-select-all-none, .bef-tree')
.find('input:checkbox').each(function () {
$(this).prop('checked', true);
// @TODO:
// _bef_highlight(this, context);
})
.end()
// attr() doesn't trigger a change event, so we do it ourselves. But just on
// one checkbox otherwise we have many spinning cursors.
.find('input[type=checkbox]:first').change();
}
else {
// Unselect all the checkboxes.
$(this)
.html(selAll)
.siblings('.bef-select-all-none, .bef-tree')
.find('input:checkbox').each(function () {
$(this).prop('checked', false);
// @TODO:
// _bef_highlight(this, context);
})
.end()
// attr() doesn't trigger a change event, so we do it ourselves. But just on
// one checkbox otherwise we have many spinning cursors.
.find('input[type=checkbox]:first').change();
}
});
// Add link to the page for each set of checkboxes.
selected
.addClass('bef-processed')
.each(function (index) {
// Clone the link prototype and insert into the DOM.
var newLink = link.clone(true);
newLink.insertBefore($(this));
// If all checkboxes are already checked by default then switch to Select None.
if ($('input:checkbox:checked', this).length == $('input:checkbox', this).length) {
newLink.text(selNone);
}
});
}
// @TODO:
// Add highlight class to checked checkboxes for better theming
// $('.bef-tree input[type="checkbox"], .bef-checkboxes input[type="checkbox"]')
// Highlight newly selected checkboxes
// .change(function () {
// _bef_highlight(this, context);
// })
// .filter(':checked').closest('.form-item', context).addClass('highlight')
// ;
// @TODO: Put this somewhere else...
// Check for and initialize datepickers
// if (Drupal.settings.better_exposed_filters.datepicker) {
// // Note: JavaScript does not treat "" as null
// if (Drupal.settings.better_exposed_filters.datepicker_options.dateformat) {
// $('.bef-datepicker').datepicker({
// dateFormat: Drupal.settings.better_exposed_filters.datepicker_options.dateformat
// });
// }
// else {
// $('.bef-datepicker').datepicker();
// }
// }
} // attach: function() {
}; // Drupal.behaviors.better_exposed_filters = {.
Drupal.behaviors.betterExposedFiltersAllNoneNested = {
attach:function (context, settings) {
$('.bef-select-all-none-nested li').has('ul').once('bef-all-none-nested').each(function () {
var $this = $(this);
// Check/uncheck child terms along with their parent.
$this.find('input:checkbox:first').change(function () {
$(this).closest('li').find('ul li input:checkbox').prop('checked', this.checked);
});
// When a child term is checked or unchecked, set the parent term's
// status as needed.
$this.find('ul input:checkbox').change(function () {
// Determine the number of unchecked sibling checkboxes.
var $this = $(this);
var uncheckedSiblings = $this.closest('li').siblings('li').find('> div > input:checkbox:not(:checked)').length;
// If this term or any siblings are unchecked, uncheck the parent and
// all ancestors.
if (uncheckedSiblings || !this.checked) {
$this.parents('ul').siblings('div').find('input:checkbox').prop('checked', false);
}
// If this and all sibling terms are checked, check the parent. Then
// trigger the parent's change event to see if that change affects the
// grandparent's checked state.
if (this.checked && !uncheckedSiblings) {
$(this).closest('ul').closest('li').find('input:checkbox:first').prop('checked', true).change();
}
});
});
}
}
})(jQuery);
/**
* @file
* bef_sliders.js
*
* Adds jQuery UI Slider functionality to an exposed filter.
*/
(function ($, Drupal, drupalSettings) {
Drupal.behaviors.better_exposed_filters_slider = {
attach: function (context, settings) {
if (drupalSettings.better_exposed_filters.slider) {
$.each(drupalSettings.better_exposed_filters.slider_options, function (i, sliderOptions) {
var data_selector = 'edit-' + sliderOptions.dataSelector;
// Collect all possible input fields for this filter.
var $inputs = $("input[data-drupal-selector=" + data_selector + "], input[data-drupal-selector=" + data_selector + "-max], input[data-drupal-selector=" + data_selector + "-min]", context).once('slider-filter');
// This is a single-value filter.
if ($inputs.length === 1) {
// This is a single-value filter.
var $input = $($inputs[0]);
// Get the default value. We use slider min if there is no default.
var defaultValue = parseFloat(($input.val() === '') ? sliderOptions.min : $input.val());
// Set the element value in case we are using the slider min.
$input.val(defaultValue);
// Build the HTML and settings for the slider.
var slider = $('<div class="bef-slider"></div>').slider({
min: parseFloat(sliderOptions.min),
max: parseFloat(sliderOptions.max),
step: parseFloat(sliderOptions.step),
animate: sliderOptions.animate ? sliderOptions.animate : false,
orientation: sliderOptions.orientation,
value: defaultValue,
slide: function (event, ui) {
$input.val(ui.value);
},
// This fires when the value is set programmatically or the stop
// event fires. This takes care of the case that a user enters a
// value into the text field that is not a valid step of the
// slider. In that case the slider will go to the nearest step and
// this change event will update the text area.
change: function (event, ui) {
$input.val(ui.value);
},
// Attach stop listeners.
stop: function (event, ui) {
// Click the auto submit button.
$(this).parents('form').find('[data-bef-auto-submit-click]').click();
}
});
$input.after(slider);
// Update the slider when the field is updated.
$input.blur(function () {
befUpdateSlider($(this), null, sliderOptions);
});
}
else if ($inputs.length === 2) {
// This is an in-between or not-in-between filter. Use a range
// filter and tie the min and max into the two input elements.
var $min = $($inputs[0]),
$max = $($inputs[1]),
// Get the default values. We use slider min & max if there are
// no defaults.
defaultMin = parseFloat(($min.val() == '') ? sliderOptions.min : $min.val()),
defaultMax = parseFloat(($max.val() == '') ? sliderOptions.max : $max.val());
// Set the element value in case we are using the slider min & max.
$min.val(defaultMin);
$max.val(defaultMax);
var slider = $('<div class="bef-slider"></div>').slider({
range: true,
min: parseFloat(sliderOptions.min),
max: parseFloat(sliderOptions.max),
step: parseFloat(sliderOptions.step),
animate: sliderOptions.animate ? sliderOptions.animate : false,
orientation: sliderOptions.orientation,
values: [defaultMin, defaultMax],
// Update the textfields as the sliders are moved.
slide: function (event, ui) {
$min.val(ui.values[0]);
$max.val(ui.values[1]);
},
// This fires when the value is set programmatically or the
// stop event fires. This takes care of the case that a user
// enters a value into the text field that is not a valid step
// of the slider. In that case the slider will go to the
// nearest step and this change event will update the text
// area.
change: function (event, ui) {
$min.val(ui.values[0]);
$max.val(ui.values[1]);
},
// Attach stop listeners.
stop: function (event, ui) {
// Click the auto submit button.
$(this).parents('form').find('.ctools-auto-submit-click').click();
}
});
$min.after(slider);
// Update the slider when the fields are updated.
$min.blur(function () {
befUpdateSlider($(this), 0, sliderOptions);
});
$max.blur(function () {
befUpdateSlider($(this), 1, sliderOptions);
});
}
})
}
}
}
/**
* Update a slider when a related input element is changed.
*
* We don't need to check whether the new value is valid based on slider min,
* max, and step because the slider will do that automatically and then we
* update the textfield on the slider's change event.
*
* We still have to make sure that the min & max values of a range slider
* don't pass each other though, however once this jQuery UI bug is fixed we
* won't have to.
*
* @see: http://bugs.jqueryui.com/ticket/3762
*
* @param $el
* A jQuery object of the updated element.
* @param valIndex
* The index of the value for a range slider or null for a non-range slider.
* @param sliderOptions
* The options for the current slider.
*/
function befUpdateSlider($el, valIndex, sliderOptions) {
var val = parseFloat($el.val()),
currentMin = $el.parents('div.views-widget').next('.bef-slider').slider('values', 0),
currentMax = $el.parents('div.views-widget').next('.bef-slider').slider('values', 1);
// If we have a range slider.
if (valIndex != null) {
// Make sure the min is not more than the current max value.
if (valIndex === 0 && val > currentMax) {
val = currentMax;
}
// Make sure the max is not more than the current max value.
if (valIndex === 1 && val < currentMin) {
val = currentMin;
}
// If the number is invalid, go back to the last value.
if (isNaN(val)) {
val = $el.parents('div.views-widget').next('.bef-slider').slider('values', valIndex);
}
}
else {
// If the number is invalid, go back to the last value.
if (isNaN(val)) {
val = $el.parents('div.views-widget').next('.bef-slider').slider('value');
}
}
// Make sure we are a number again.
val = parseFloat(val, 10);
// Set the slider to the new value.
// The slider's change event will then update the textfield again so that
// they both have the same value.
if (valIndex != null) {
$el.parents('div.views-widget').next('.bef-slider').slider('values', valIndex, val);
}
else {
$el.parents('div.views-widget').next('.bef-slider').slider('value', val);
}
}
})(jQuery, Drupal, drupalSettings);
/**
* @file
* better_exposed_filters.js
*
* Provides some client-side functionality for the Better Exposed Filters module.
*/
(function ($, Drupal, drupalSettings) {
Drupal.behaviors.betterExposedFilters = {
attach: function (context, settings) {
// Add highlight class to checked checkboxes for better theming.
$('.bef-tree input[type=checkbox], .bef-checkboxes input[type=checkbox]')
// Highlight newly selected checkboxes.
.change(function () {
_bef_highlight(this, context);
})
.filter(':checked').closest('.form-item', context).addClass('highlight');
}
};
/*
* Helper functions
*/
/**
* Adds/Removes the highlight class from the form-item div as appropriate.
*/
function _bef_highlight(elem, context) {
$elem = $(elem, context);
$elem.attr('checked')
? $elem.closest('.form-item', context).addClass('highlight')
: $elem.closest('.form-item', context).removeClass('highlight');
}
})(jQuery, Drupal, drupalSettings);
<?xml version="1.0"?>
<ruleset name="Drupal 8 coding standards">
<description>Drupal 8 coding standards</description>
<exclude-pattern>*/.git/*</exclude-pattern>
<exclude-pattern>*/config/*</exclude-pattern>
<exclude-pattern>*/icons/*</exclude-pattern>
<exclude-pattern>*/vendor/*</exclude-pattern>
<rule ref="Drupal"/>
</ruleset>
<?xml version="1.0" encoding="UTF-8"?>
<!-- This based on Drupal's core phpunit.xml.dist. -->
<!-- TODO set checkForUnintentionallyCoveredCode="true" once https://www.drupal.org/node/2626832 is resolved. -->
<!-- PHPUnit expects functional tests to be run with either a privileged user
or your current system user. See core/tests/README.md and
https://www.drupal.org/node/2116263 for details.
-->
<phpunit bootstrap="tests/bootstrap.php" colors="true"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutChangesToGlobalState="true"
convertDeprecationsToExceptions="false">
<!-- TODO set printerClass="\Drupal\Tests\Listeners\HtmlOutputPrinter" once
https://youtrack.jetbrains.com/issue/WI-24808 is resolved. Drupal provides a
result printer that links to the html output results for functional tests.
Unfortunately, this breaks the output of PHPStorm's PHPUnit runner. However, if
using the command line you can add
- -printer="\Drupal\Tests\Listeners\HtmlOutputPrinter" to use it (note there
should be no spaces between the hyphens).
-->
<php>
<!-- Set error reporting to E_ALL. -->
<ini name="error_reporting" value="32767"/>
<!-- Do not limit the amount of memory tests take to run. -->
<ini name="memory_limit" value="-1"/>
<!-- Example SIMPLETEST_BASE_URL value: http://localhost -->
<env name="SIMPLETEST_BASE_URL" value=""/>
<!-- Example SIMPLETEST_DB value: mysql://username:password@localhost/databasename#table_prefix -->
<env name="SIMPLETEST_DB" value=""/>
<!-- Example BROWSERTEST_OUTPUT_DIRECTORY value: /path/to/webroot/sites/simpletest/browser_output -->
<env name="BROWSERTEST_OUTPUT_DIRECTORY" value=""/>
<!-- To disable deprecation testing completely uncomment the next line. -->
<env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled"/>
<!-- Example for changing the driver class for mink tests MINK_DRIVER_CLASS value: 'Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver' -->
<!-- Example for changing the driver args to mink tests MINK_DRIVER_ARGS value: '["http://127.0.0.1:8510"]' -->
<!-- Example for changing the driver args to phantomjs tests MINK_DRIVER_ARGS_PHANTOMJS value: '["http://127.0.0.1:8510"]' -->
<!-- Example for changing the driver args to webdriver tests MINK_DRIVER_ARGS_WEBDRIVER value: '["firefox", null, "http://localhost:4444/wd/hub"]' -->
</php>
<testsuites>
<testsuite name="unit">
<file>./tests/TestSuites/UnitTestSuite.php</file>
</testsuite>
<testsuite name="kernel">
<file>./tests/TestSuites/KernelTestSuite.php</file>
</testsuite>
<testsuite name="functional">
<file>./tests/TestSuites/FunctionalTestSuite.php</file>
</testsuite>
<testsuite name="nonfunctional">
<file>./tests/TestSuites/UnitTestSuite.php</file>
<file>./tests/TestSuites/KernelTestSuite.php</file>
</testsuite>
<testsuite name="functional-javascript">
<file>./tests/TestSuites/FunctionalJavascriptTestSuite.php</file>
</testsuite>
</testsuites>
<listeners>
<listener class="\Drupal\Tests\Listeners\DrupalListener">
</listener>
<!-- The Symfony deprecation listener has to come after the Drupal listener -->
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
</listener>
</listeners>
<!-- Filter for coverage reports. -->
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory>../modules/better_exposed_filters</directory>
<!-- By definition test classes have no tests. -->
<exclude>
<directory>../modules/better_exposed_filters/tests</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
<?php
namespace Drupal\better_exposed_filters\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Better exposed filters widget item annotation object.
*
* @see \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersFilterWidgetManager
* @see plugin_api
*
* @Annotation
*/
class BetterExposedFiltersFilterWidget extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The label of the plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
}
<?php
namespace Drupal\better_exposed_filters\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Better exposed filters widget item annotation object.
*
* @see \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager
* @see plugin_api
*
* @Annotation
*/
class BetterExposedFiltersPagerWidget extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The label of the plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
}
<?php
namespace Drupal\better_exposed_filters\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Better exposed filters sort widget item annotation object.
*
* @see \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersSortWidgetManager
* @see plugin_api
*
* @Annotation
*/
class BetterExposedFiltersSortWidget extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The label of the plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
}
<?php
namespace Drupal\better_exposed_filters;
/**
* Defines a helper class for better exposed filters.
*/
class BetterExposedFiltersHelper {
/**
* Rewrites a set of options given a string from the config form.
*
* Rewrites should be specified, one per line, using the format
* old_string|new_string. If new_string is empty, the option will be removed.
*
* @param array $options
* An array of key => value pairs that may be rewritten.
* @param string $rewrite_settings
* String representing the entry in the settings form.
* @param bool $reorder
* Reorder $options based on the rewrite settings.
*
* @return array
* Rewritten $options.
*/
public static function rewriteOptions(array $options, $rewrite_settings, $reorder = FALSE) {
// Break out early if we don't have anything to rewrite.
if (empty($rewrite_settings) || !is_string($rewrite_settings)) {
return $options;
}
$rewrites = [];
$order = [];
$return = [];
// Get a copy of the option, flattened with their keys preserved.
$flat_options = self::flattenOptions($options, TRUE);
// Preserve order.
if (!$reorder) {
$order = array_keys($options);
}
$lines = explode("\n", trim($rewrite_settings));
foreach ($lines as $line) {
list($search, $replace) = array_map('trim', explode('|', $line));
if (!empty($search)) {
$rewrites[$search] = $replace;
// Find the key of the option we need to reorder.
if ($reorder) {
$key = array_search($search, $flat_options);
if ($key !== FALSE) {
$order[] = $key;
}
}
}
}
// Reorder options in the order they are specified in rewrites.
// Collect the keys to use later.
if ($reorder && !empty($order)) {
// Start with the items that were listed in the rewrite settings.
foreach ($order as $key) {
$return[$key] = $options[$key];
unset($options[$key]);
}
// Tack remaining options on the end.
$return += $options;
}
else {
$return = $options;
}
// Rewrite the option value.
foreach ($return as $index => &$choice) {
if (is_object($choice) && isset($choice->option)) {
$key = key($choice->option);
$value = &$choice->option[$key];
}
else {
$choice = (string) $choice;
$value = &$choice;
}
if (!is_scalar($value)) {
// We give up...
continue;
}
if (isset($rewrites[$value])) {
if ('' === $rewrites[$value]) {
unset($return[$index]);
}
else {
$value = $rewrites[$value];
}
}
}
return $return;
}
/**
* Flattens list of mixed options into a simple array of scalar value.
*
* @param array $options
* List of mixed options - scalar, translatable markup or taxonomy term
* options.
* @param bool $preserve_keys
* TRUE if the original keys should be preserved, FALSE otherwise.
*
* @return array
* Flattened list of scalar options.
*/
public static function flattenOptions(array $options, $preserve_keys = FALSE) {
$flat_options = [];
foreach ($options as $key => $choice) {
if (is_array($choice)) {
$flat_options = array_merge($flat_options, self::flattenOptions($choice));
}
elseif (is_object($choice) && isset($choice->option)) {
$key = $preserve_keys ? $key : key($choice->option);
$flat_options[$key] = current($choice->option);
}
else {
$flat_options[$key] = (string) $choice;
}
}
return $flat_options;
}
/**
* Sort options alphabetically.
*
* @param array $options
* Array of unsorted options - scalar, translatable markup or taxonomy term
* options.
*
* @return array
* Alphabetically sorted array of original values.
*/
public static function sortOptions(array $options) {
// Flatten array of mixed values to a simple array of scalar values.
$flat_options = self::flattenOptions($options, TRUE);
// Alphabetically sort our list of concatenated values.
asort($flat_options);
// Now use its keys to sort the original array.
return array_replace(array_flip(array_keys($flat_options)), $options);
}
/**
* Sort nested options alphabetically.
*
* @param array $options
* Array of nested unsorted options - scalar, translatable markup or
* taxonomy term options.
* @param string $delimiter
* The delimiter used to indicate nested level. (e.g. -Seattle)
*
* @return array
* Alphabetically sorted array of original values.
*/
public static function sortNestedOptions(array $options, $delimiter = '-') {
// Flatten array of mixed values to a simple array of scalar values.
$flat_options = self::flattenOptions($options, TRUE);
$prev_key = NULL;
$level = 0;
$parent = [$level => ''];
// Iterate over each option.
foreach ($flat_options as $key => &$choice) {
// For each option, determine the nested level based on the delimiter.
// Example:
// - 'United States' will have level 0.
// - '-Seattle' will have level 1.
$cur_level = strlen($choice) - strlen(ltrim($choice, $delimiter));
// If we are going down a level, keep track of its parent value.
if ($cur_level > $level) {
$parent[$cur_level] = $flat_options[$prev_key];
}
// Prepend each option value with its parent for easier sorting.
// Example:
// '-Seattle' is below 'United States', its concatenated value will become
// 'United States-Seattle' etc...
$choice = $parent[$cur_level] . $choice;
// Update level and prev_key.
$level = $cur_level;
$prev_key = $key;
}
// Alphabetically sort our list of concatenated values.
asort($flat_options);
// Now use its keys to sort the original array.
return array_replace(array_flip(array_keys($flat_options)), $options);
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\views\Plugin\views\ViewsHandlerInterface;
use Drupal\views\ViewExecutable;
/**
* Base class for Better exposed filters widget plugins.
*/
abstract class BetterExposedFiltersWidgetBase extends PluginBase implements BetterExposedFiltersWidgetInterface {
use StringTranslationTrait;
/**
* The views executable object.
*
* @var \Drupal\views\ViewExecutable
*/
protected $view;
/**
* The views plugin this configuration will affect when exposed.
*
* @var \Drupal\views\Plugin\views\ViewsHandlerInterface
*/
protected $handler;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $configuration);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'plugin_id' => $this->pluginId,
];
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
$this->configuration = $configuration;
}
/**
* {@inheritdoc}
*/
public function setView(ViewExecutable $view) {
$this->view = $view;
}
/**
* {@inheritdoc}
*/
public function setViewsHandler(ViewsHandlerInterface $handler) {
$this->handler = $handler;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
// Validation is optional.
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
// Apply submitted form state to configuration.
$values = $form_state->getValues();
foreach ($values as $key => $value) {
if (array_key_exists($key, $this->configuration)) {
$this->configuration[$key] = $value;
}
else {
// Remove from form state.
unset($values[$key]);
}
}
}
/*
* Helper functions.
*/
/**
* Sets metadata on the form elements for easier processing.
*
* @param array $element
* The form element to apply the metadata to.
*
* @see ://www.drupal.org/project/drupal/issues/2511548
*/
protected function addContext(array &$element) {
$element['#context'] = [
'#plugin_type' => 'bef',
'#plugin_id' => $this->pluginId,
'#view_id' => $this->view->id(),
'#display_id' => $this->view->current_display,
];
}
/**
* Moves an exposed form element into a field group.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Exposed views form state.
* @param string $element
* The key of the form element.
* @param string $group
* The name of the group element.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the instance cannot be created, such as if the ID is invalid.
*/
protected function addElementToGroup(array &$form, FormStateInterface $form_state, $element, $group) {
// Ensure group is enabled.
$form[$group]['#access'] = TRUE;
// Add element to group.
$form[$element]['#group'] = $group;
// Persist state of collapsible field-sets with active elements.
if (empty($form[$group]['#open'])) {
// Use raw user input to determine if field-set should be open or closed.
$user_input = $form_state->getUserInput()[$element] ?? [0];
// Take multiple values into account.
if (!is_array($user_input)) {
$user_input = [$user_input];
}
// Check if one or more values are set for our current element.
$options = $form[$element]['#options'] ?? [];
$default_value = $form[$element]['#default_value'] ?? key($options);
$has_values = array_reduce($user_input, function ($carry, $value) use ($form, $element, $default_value) {
return $carry || ($value === $default_value ? '' : ($value || $default_value === 0));
}, FALSE);
if ($has_values) {
$form[$group]['#open'] = TRUE;
}
}
}
/**
* Returns exposed form action URL object.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Exposed views form state.
*
* @return \Drupal\Core\Url
* Url object.
*/
protected function getExposedFormActionUrl(FormStateInterface $form_state) {
/** @var \Drupal\views\ViewExecutable $view */
$view = $form_state->get('view');
$display = $form_state->get('display');
if (isset($display['display_options']['path'])) {
return Url::fromRoute(implode('.', [
'view',
$view->id(),
$display['id'],
]));
}
$request = \Drupal::request();
$url = Url::createFromRequest(clone $request);
$url->setAbsolute();
return $url;
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\views\Plugin\views\ViewsHandlerInterface;
use Drupal\views\ViewExecutable;
/**
* Defines an interface for Better exposed filters filter widget plugins.
*/
interface BetterExposedFiltersWidgetInterface extends PluginFormInterface, PluginInspectionInterface, ConfigurableInterface {
/**
* Sets the view object.
*
* @param \Drupal\views\ViewExecutable $view
* The views executable object.
*/
public function setView(ViewExecutable $view);
/**
* Sets the exposed view handler plugin.
*
* @param \Drupal\views\Plugin\views\ViewsHandlerInterface $handler
* The views handler plugin this configuration will affect when exposed.
*/
public function setViewsHandler(ViewsHandlerInterface $handler);
/**
* Verify this plugin can be used on the form element.
*
* @param mixed $handler
* The handler type we are altering (e.g. filter, pager, sort).
* @param array $options
* The options for this handler.
*
* @return bool
* If this plugin can be used.
*/
public static function isApplicable($handler = NULL, array $options = []);
/**
* Manipulate views exposed from element.
*
* @param array $form
* The views configuration form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state.
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state);
}
<?php
namespace Drupal\better_exposed_filters\Plugin;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\DependencyInjection\Container;
/**
* Provides the Better exposed filters widget plugin manager.
*/
class BetterExposedFiltersWidgetManager extends DefaultPluginManager {
/**
* The widget type.
*
* @var string
*/
protected $type;
/**
* Constructs a new BetterExposedFiltersFilterWidgetManager object.
*
* @param string $type
* The plugin type, for example filter, pager or sort.
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
$plugin_interface = 'Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface';
$plugin_definition_annotation_name = 'Drupal\better_exposed_filters\Annotation\BetterExposedFilters' . Container::camelize($type) . 'Widget';
parent::__construct("Plugin/better_exposed_filters/$type", $namespaces, $module_handler, $plugin_interface, $plugin_definition_annotation_name);
$this->type = $type;
$this->alterInfo('better_exposed_filters_better_exposed_filters_' . $type . '_widget_info');
$this->setCacheBackend($cache_backend, 'better_exposed_filters:' . $type . '_widget');
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter;
use Drupal\Core\Form\FormStateInterface;
/**
* JQuery UI date picker widget implementation.
*
* @BetterExposedFiltersFilterWidget(
* id = "bef_datepicker",
* label = @Translation("jQuery UI Date Picker"),
* )
*/
class DatePickers extends FilterWidgetBase {
/**
* {@inheritdoc}
*/
public static function isApplicable($filter = NULL, array $filter_options = []) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$is_applicable = FALSE;
if ((is_a($filter, 'Drupal\views\Plugin\views\filter\Date') || !empty($filter->date_handler)) && !$filter->isAGroup()) {
$is_applicable = TRUE;
}
return $is_applicable;
}
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
$field_id = $this->getExposedFilterFieldId();
parent::exposedFormAlter($form, $form_state);
// Attach the JS (@see /js/datepickers.js)
$form['#attached']['library'][] = 'better_exposed_filters/datepickers';
// Date picker settings.
$form[$field_id]['#attached']['drupalSettings']['better_exposed_filters']['datepicker'] = TRUE;
$form[$field_id]['#attached']['drupalSettings']['better_exposed_filters']['datepicker_options'] = [];
$drupal_settings = &$form[$field_id]['#attached']['drupalSettings']['better_exposed_filters']['datepicker_options'];
// Single Date API-based input element.
$is_single_date = isset($form[$field_id]['value']['#type'])
&& 'date_text' == $form[$field_id]['value']['#type'];
// Double Date-API-based input elements such as "in-between".
$is_double_date = isset($form[$field_id]['min']) && isset($form[$field_id]['max'])
&& 'date_text' == $form[$field_id]['min']['#type']
&& 'date_text' == $form[$field_id]['max']['#type'];
if ($is_single_date || $is_double_date) {
if (isset($form[$field_id]['value'])) {
$format = $form[$field_id]['value']['#date_format'];
$form[$field_id]['value']['#attributes']['class'][] = 'bef-datepicker';
}
else {
// Both min and max share the same format.
$format = $form[$field_id]['min']['#date_format'];
$form[$field_id]['min']['#attributes']['class'][] = 'bef-datepicker';
$form[$field_id]['max']['#attributes']['class'][] = 'bef-datepicker';
}
// Convert Date API format to jQuery UI date format.
$mapping = $this->getjQueryUiDateFormatting();
$drupal_settings['dateformat'] = str_replace(array_keys($mapping), array_values($mapping), $format);
}
else {
/*
* Standard Drupal date field. Depending on the settings, the field
* can be at $form[$field_id] (single field) or
* $form[$field_id][subfield] for two-value date fields or filters
* with exposed operators.
*/
$fields = ['min', 'max', 'value'];
if (count(array_intersect($fields, array_keys($form[$field_id])))) {
foreach ($fields as $field) {
if (isset($form[$field_id][$field])) {
$form[$field_id][$field]['#attributes']['class'][] = 'bef-datepicker';
}
}
}
else {
$form[$field_id]['#attributes']['class'][] = 'bef-datepicker';
}
}
}
/**
* Convert Date API formatting to jQuery formatDate formatting.
*
* @TODO: To be honest, I'm not sure this is needed. Can you set a
* Date API field to accept anything other than Y-m-d? Well, better
* safe than sorry...
*
* @see http://us3.php.net/manual/en/function.date.php
* @see http://docs.jquery.com/UI/Datepicker/formatDate
*
* @return array
* PHP date format => jQuery formatDate format
* (comments are for the PHP format, lines that are commented out do
* not have a jQuery formatDate equivalent, but maybe someday they
* will...)
*/
private function getjQueryUiDateFormatting() {
return [
/* Day */
// Day of the month, 2 digits with leading zeros 01 to 31.
'd' => 'dd',
// A textual representation of a day, three letters Mon through
// Sun.
'D' => 'D',
// Day of the month without leading zeros 1 to 31.
'j' => 'd',
// (lowercase 'L') A full textual representation of the day of the
// week Sunday through Saturday.
'l' => 'DD',
// ISO-8601 numeric representation of the day of the week (added
// in PHP 5.1.0) 1 (for Monday) through 7 (for Sunday).
// 'N' => ' ',
// English ordinal suffix for the day of the month, 2 characters
// st, nd, rd or th. Works well with j.
// 'S' => ' ',
// Numeric representation of the day of the week 0 (for Sunday)
// through 6 (for Saturday).
// 'w' => ' ',
// The day of the year (starting from 0) 0 through 365.
'z' => 'o',
/* Week */
// ISO-8601 week number of year, weeks starting on Monday (added
// in PHP 4.1.0) Example: 42 (the 42nd week in the year).
// 'W' => ' ',.
/* Month */
// A full textual representation of a month, such as January or
// March January through December.
'F' => 'MM',
// Numeric representation of a month, with leading zeros 01
// through 12.
'm' => 'mm',
// A short textual representation of a month, three letters Jan
// through Dec.
'M' => 'M',
// Numeric representation of a month, without leading zeros 1
// through 12.
'n' => 'm',
// Number of days in the given month 28 through 31.
// 't' => ' ',.
/* Year */
// Whether it's a leap year 1 if it is a leap year, 0 otherwise.
// 'L' => ' ',
// ISO-8601 year number. This has the same value as Y, except that
// if the ISO week number (W) belongs to the previous or next
// year, that year is used instead. (added in PHP 5.1.0).
// Examples: 1999 or 2003.
// 'o' => ' ',
// A full numeric representation of a year, 4 digits Examples:
// 1999 or 2003.
'Y' => 'yy',
// A two digit representation of a year Examples: 99 or 03.
'y' => 'y',
/* Time */
// Lowercase Ante meridiem and Post meridiem am or pm.
// 'a' => ' ',
// Uppercase Ante meridiem and Post meridiem AM or PM.
// 'A' => ' ',
// Swatch Internet time 000 through 999.
// 'B' => ' ',
// 12-hour format of an hour without leading zeros 1 through 12.
// 'g' => ' ',
// 24-hour format of an hour without leading zeros 0 through 23.
// 'G' => ' ',
// 12-hour format of an hour with leading zeros 01 through 12.
// 'h' => ' ',
// 24-hour format of an hour with leading zeros 00 through 23.
// 'H' => ' ',
// Minutes with leading zeros 00 to 59.
// 'i' => ' ',
// Seconds, with leading zeros 00 through 59.
// 's' => ' ',
// Microseconds (added in PHP 5.2.2) Example: 654321.
// 'u' => ' ',.
];
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter;
use Drupal\Core\Form\FormStateInterface;
/**
* Default widget implementation.
*
* @BetterExposedFiltersFilterWidget(
* id = "default",
* label = @Translation("Default"),
* )
*/
class DefaultWidget extends FilterWidgetBase {
/**
* {@inheritdoc}
*/
public static function isApplicable($filter = NULL, array $filter_options = []) {
return TRUE;
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter;
use Drupal\Core\Form\FormStateInterface;
/**
* Default widget implementation.
*
* @BetterExposedFiltersFilterWidget(
* id = "bef_hidden",
* label = @Translation("Hidden"),
* )
*/
class Hidden extends FilterWidgetBase {
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
$field_id = $this->getExposedFilterFieldId();
parent::exposedFormAlter($form, $form_state);
if (empty($form[$field_id]['#multiple'])) {
// Single entry filters can simply be changed to a different element
// type.
$form[$field_id]['#type'] = 'hidden';
}
else {
// Hide the label.
$form['#info']["filter-$field_id"]['label'] = '';
$form[$field_id]['#title'] = '';
// Use BEF's preprocess and template to output the hidden elements.
$form[$field_id]['#theme'] = 'bef_hidden';
}
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter;
use Drupal\better_exposed_filters\BetterExposedFiltersHelper;
use Drupal\Core\Form\FormStateInterface;
/**
* Default widget implementation.
*
* @BetterExposedFiltersFilterWidget(
* id = "bef_links",
* label = @Translation("Links"),
* )
*/
class Links extends FilterWidgetBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'select_all_none' => FALSE,
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$filter = $this->handler;
$form = parent::buildConfigurationForm($form, $form_state);
$form['select_all_none'] = [
'#type' => 'checkbox',
'#title' => $this->t('Add select all/none links'),
'#default_value' => !empty($this->configuration['select_all_none']),
'#disabled' => !$filter->options['expose']['multiple'],
'#description' => $this->t('Add a "Select All/None" link when rendering the exposed filter using checkboxes. If this option is disabled, edit the filter and check the "Allow multiple selections".'
),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$filter = $this->handler;
$field_id = $this->getExposedFilterFieldId();
parent::exposedFormAlter($form, $form_state);
if (!empty($form[$field_id])) {
// Clean up filters that pass objects as options instead of strings.
if (!empty($form[$field_id]['#options'])) {
$form[$field_id]['#options'] = BetterExposedFiltersHelper::flattenOptions($form[$field_id]['#options']);
}
// Support rendering hierarchical links (e.g. taxonomy terms).
if (!empty($filter->options['hierarchy'])) {
$form[$field_id]['#bef_nested'] = TRUE;
}
$form[$field_id]['#theme'] = 'bef_links';
// Exposed form displayed as blocks can appear on pages other than
// the view results appear on. This can cause problems with
// select_as_links options as they will use the wrong path. We
// provide a hint for theme functions to correct this.
$form[$field_id]['#bef_path'] = $this->getExposedFormActionUrl($form_state);
}
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter;
use Drupal\better_exposed_filters\BetterExposedFiltersHelper;
use Drupal\Core\Form\FormStateInterface;
/**
* Default widget implementation.
*
* @BetterExposedFiltersFilterWidget(
* id = "bef",
* label = @Translation("Checkboxes/Radio Buttons"),
* )
*/
class RadioButtons extends FilterWidgetBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'select_all_none' => FALSE,
'select_all_none_nested' => FALSE,
'display_inline' => FALSE,
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$filter = $this->handler;
$form = parent::buildConfigurationForm($form, $form_state);
$form['select_all_none'] = [
'#type' => 'checkbox',
'#title' => $this->t('Add select all/none links'),
'#default_value' => !empty($this->configuration['select_all_none']),
'#disabled' => !$filter->options['expose']['multiple'],
'#description' => $this->t('Add a "Select All/None" link when rendering the exposed filter using checkboxes. If this option is disabled, edit the filter and check the "Allow multiple selections".'
),
];
$form['select_all_none_nested'] = [
'#type' => 'checkbox',
'#title' => $this->t('Add nested all/none selection'),
'#default_value' => !empty($this->configuration['select_all_none_nested']),
'#disabled' => (!$filter->options['expose']['multiple']) || (isset($filter->options['hierarchy']) && !$filter->options['hierarchy']),
'#description' => $this->t('When a parent checkbox is checked, check all its children. If this option is disabled, edit the filter and check "Allow multiple selections" and edit the filter settings and check "Show hierarchy in dropdown".'
),
];
$form['display_inline'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display inline'),
'#default_value' => !empty($this->configuration['display_inline']),
'#description' => $this->t('Display checkbox/radio options inline.'
),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$filter = $this->handler;
// Form element is designated by the element ID which is user-
// configurable.
$field_id = $filter->options['is_grouped'] ? $filter->options['group_info']['identifier'] : $filter->options['expose']['identifier'];
parent::exposedFormAlter($form, $form_state);
if (!empty($form[$field_id])) {
// Clean up filters that pass objects as options instead of strings.
if (!empty($form[$field_id]['#options'])) {
$form[$field_id]['#options'] = BetterExposedFiltersHelper::flattenOptions($form[$field_id]['#options']);
}
// Support rendering hierarchical checkboxes/radio buttons (e.g. taxonomy
// terms).
if (!empty($filter->options['hierarchy'])) {
$form[$field_id]['#bef_nested'] = TRUE;
}
// Display inline.
$form[$field_id]['#bef_display_inline'] = $this->configuration['display_inline'];
// Render as checkboxes if filter allows multiple selections.
if (!empty($form[$field_id]['#multiple'])) {
$form[$field_id]['#theme'] = 'bef_checkboxes';
$form[$field_id]['#type'] = 'checkboxes';
// Show all/none option.
$form[$field_id]['#bef_select_all_none'] = $this->configuration['select_all_none'];
$form[$field_id]['#bef_select_all_none_nested'] = $this->configuration['select_all_none_nested'];
// Attach the JS (@see /js/bef_select_all_none.js)
$form['#attached']['library'][] = 'better_exposed_filters/select_all_none';
}
// Else render as radio buttons.
else {
$form[$field_id]['#theme'] = 'bef_radios';
$form[$field_id]['#type'] = 'radios';
}
}
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter;
use Drupal\Core\Form\FormStateInterface;
/**
* Single on/off widget implementation.
*
* @BetterExposedFiltersFilterWidget(
* id = "bef_single",
* label = @Translation("Single On/Off Checkbox"),
* )
*/
class Single extends FilterWidgetBase {
/**
* {@inheritdoc}
*/
public static function isApplicable($filter = NULL, array $filter_options = []) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$is_applicable = FALSE;
// Sanity check to ensure we have a filter to work with.
if (!isset($filter)) {
return $is_applicable;
}
if (is_a($filter, 'Drupal\views\Plugin\views\filter\BooleanOperator') || ($filter->isAGroup() && count($filter->options['group_info']['group_items']) == 1)) {
$is_applicable = TRUE;
}
return $is_applicable;
}
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$filter = $this->handler;
// Form element is designated by the element ID which is user-
// configurable, and stored differently for grouped filters.
$exposed_id = $filter->options['expose']['identifier'];
$field_id = $this->getExposedFilterFieldId();
parent::exposedFormAlter($form, $form_state);
if (!empty($form[$field_id])) {
// Views populates missing values in $form_state['input'] with the
// defaults and a checkbox does not appear in $_GET (or $_POST) so it
// will appear to be missing when a user submits a form. Because of
// this, instead of unchecking the checkbox value will revert to the
// default. More, the default value for select values (i.e. 'Any') is
// reused which results in the checkbox always checked.
$input = $form_state->getUserInput();
// The input value ID is not always consistent.
// Prioritize the field ID, but default to exposed ID.
// @todo Remove $exposed_id once
// https://www.drupal.org/project/drupal/issues/288429 is fixed.
$input_value = isset($input[$field_id]) ? $input[$field_id] : (isset($input[$exposed_id]) ? $input[$exposed_id] : NULL);
$checked = FALSE;
// We need to be super careful when working with raw input values. Let's
// make sure the value exists in our list of possible options.
if (in_array($input_value, array_keys($form[$field_id]['#options'])) && $input_value !== 'All') {
$checked = (bool) $input_value;
}
$form[$field_id]['#type'] = 'checkbox';
$form[$field_id]['#default_value'] = 0;
$form[$field_id]['#return_value'] = 1;
$form[$field_id]['#value'] = $checked ? 1 : 0;
}
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
/**
* JQuery UI slider widget implementation.
*
* @BetterExposedFiltersFilterWidget(
* id = "bef_sliders",
* label = @Translation("jQuery UI Slider"),
* )
*/
class Sliders extends FilterWidgetBase {
// Slider animation options.
const ANIMATE_NONE = 'none';
const ANIMATE_SLOW = 'slow';
const ANIMATE_NORMAL = 'normal';
const ANIMATE_FAST = 'fast';
const ANIMATE_CUSTOM = 'custom';
// Slider orientation options.
const ORIENTATION_HORIZONTAL = 'horizontal';
const ORIENTATION_VERTICAL = 'vertical';
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'min' => 0,
'max' => 99999,
'step' => 1,
'animate' => self::ANIMATE_NONE,
'animate_ms' => 0,
'orientation' => self::ORIENTATION_HORIZONTAL,
];
}
/**
* {@inheritdoc}
*/
public static function isApplicable($filter = NULL, array $filter_options = []) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$is_applicable = FALSE;
// The date filter handler extends the numeric filter handler so we have
// to exclude it specifically.
$is_numeric_filter = is_a($filter, 'Drupal\views\Plugin\views\filter\NumericFilter');
$is_range_filter = is_a($filter, 'Drupal\range\Plugin\views\filter\Range');
$is_date_filter = is_a($filter, 'Drupal\views\Plugin\views\filter\Date');
if (($is_numeric_filter || $is_range_filter) && !$is_date_filter && !$filter->isAGroup()) {
$is_applicable = TRUE;
}
return $is_applicable;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$filter = $this->handler;
$form = parent::buildConfigurationForm($form, $form_state);
$form['min'] = [
'#type' => 'number',
'#title' => $this->t('Range minimum'),
'#default_value' => $this->configuration['min'],
'#description' => $this->t('The minimum allowed value for the jQuery range slider. It can be positive, negative, or zero and have up to 11 decimal places.'),
];
$form['max'] = [
'#type' => 'number',
'#title' => $this->t('Range maximum'),
'#default_value' => $this->configuration['max'],
'#description' => $this->t('The maximum allowed value for the jQuery range slider. It can be positive, negative, or zero and have up to 11 decimal places.'),
];
$form['step'] = [
'#type' => 'number',
'#title' => $this->t('Step'),
'#default_value' => $this->configuration['step'],
'#description' => $this->t('Determines the size or amount of each interval or step the slider takes between the min and max.') . '<br />' . $this->t('The full specified value range of the slider (Range maximum - Range minimum) must be evenly divisible by the step.') . '<br />' . $this->t('The step must be a positive number of up to 5 decimal places.'),
'#min' => 0,
];
$form['animate'] = [
'#type' => 'select',
'#title' => $this->t('Animation speed'),
'#options' => [
self::ANIMATE_NONE => $this->t('None'),
self::ANIMATE_SLOW => $this->t('Slow'),
self::ANIMATE_NORMAL => $this->t('Normal'),
self::ANIMATE_FAST => $this->t('Fast'),
self::ANIMATE_CUSTOM => $this->t('Custom'),
],
'#default_value' => $this->configuration['animate'],
'#description' => $this->t('Whether to slide handle smoothly when user click outside handle on the bar.'),
];
$form['animate_ms'] = [
'#type' => 'number',
'#title' => $this->t('Animation speed in milliseconds'),
'#default_value' => $this->configuration['animate_ms'],
'#description' => $this->t('The number of milliseconds to run the animation (e.g. 1000).'),
'#states' => [
'visible' => [
':input[name="exposed_form_options[bef][filter][' . $filter->field . '][configuration][animate]"]' => ['value' => self::ANIMATE_CUSTOM],
],
],
];
$form['orientation'] = [
'#type' => 'select',
'#title' => $this->t('Orientation'),
'#options' => [
self::ORIENTATION_HORIZONTAL => $this->t('Horizontal'),
self::ORIENTATION_VERTICAL => $this->t('Vertical'),
],
'#default_value' => $this->configuration['orientation'],
'#description' => $this->t('The orientation of the jQuery range slider.'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::validateConfigurationForm($form, $form_state);
// Max must be > min.
$min = $form_state->getValue('min');
$max = $form_state->getValue('max');
if ($max <= $min) {
$form_state->setError($form['max'], $this->t('The slider max value must be greater than the slider min value.'));
}
// Step must have:
// - No more than 5 decimal places.
// - Slider range must be evenly divisible by step.
$step = $form_state->getValue('step');
if (strlen(substr(strrchr((string) $step, '.'), 1)) > 5) {
$form_state->setError($form['step'], $this->t('The slider step option for %name cannot have more than 5 decimal places.'));
}
// Very small step and a vary large range can go beyond the max value of
// an int in PHP. Thus we look for a decimal point when casting the result
// to a string.
if (strpos((string) ($max - $min) / $step, '.')) {
$form_state->setError($form['step'], $this->t('The slider range must be evenly divisible by the step option.'));
}
}
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
$field_id = $this->getExposedFilterFieldId();
parent::exposedFormAlter($form, $form_state);
// Attach the JS (@see /js/sliders.js)
$form[$field_id]['#attached']['library'][] = 'better_exposed_filters/sliders';
// Set the slider settings.
$form[$field_id]['#attached']['drupalSettings']['better_exposed_filters']['slider'] = TRUE;
$form[$field_id]['#attached']['drupalSettings']['better_exposed_filters']['slider_options'][$field_id] = [
'min' => $this->configuration['min'],
'max' => $this->configuration['max'],
'step' => $this->configuration['step'],
'animate' => ($this->configuration['animate'] === self::ANIMATE_CUSTOM) ? $this->configuration['animate_ms'] : $this->configuration['animate'],
'orientation' => $this->configuration['orientation'],
'id' => Html::getUniqueId($field_id),
'dataSelector' => Html::getId($field_id),
'viewId' => $form['#id'],
];
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\pager;
/**
* Default widget implementation.
*
* @BetterExposedFiltersPagerWidget(
* id = "default",
* label = @Translation("Default"),
* )
*/
class DefaultWidget extends PagerWidgetBase {
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\pager;
use Drupal\Core\Form\FormStateInterface;
/**
* Radio Buttons pager widget implementation.
*
* @BetterExposedFiltersPagerWidget(
* id = "bef_links",
* label = @Translation("Links"),
* )
*/
class Links extends PagerWidgetBase {
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
parent::exposedFormAlter($form, $form_state);
if (!empty($form['items_per_page'] && count($form['items_per_page']['#options']) > 1)) {
$form['items_per_page']['#theme'] = 'bef_links';
$form['items_per_page']['#items_per_page'] = max($form['items_per_page']['#default_value'], key($form['items_per_page']['#options']));
// Exposed form displayed as blocks can appear on pages other than
// the view results appear on. This can cause problems with
// select_as_links options as they will use the wrong path. We
// provide a hint for theme functions to correct this.
$form['items_per_page']['#bef_path'] = $this->getExposedFormActionUrl($form_state);
}
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\pager;
use Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetBase;
use Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Base class for Better exposed pager widget plugins.
*/
abstract class PagerWidgetBase extends BetterExposedFiltersWidgetBase implements BetterExposedFiltersWidgetInterface {
use StringTranslationTrait;
/**
* List of available exposed sort form element keys.
*
* @var array
*/
protected $pagerElements = [
'items_per_page',
'offset',
];
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'advanced' => [
'is_secondary' => FALSE,
],
];
}
/**
* {@inheritdoc}
*/
public static function isApplicable($handler = NULL, array $options = []) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = [];
$form['advanced']['is_secondary'] = [
'#type' => 'checkbox',
'#title' => $this->t('This is a secondary option'),
'#default_value' => !empty($this->configuration['advanced']['is_secondary']),
'#states' => [
'visible' => [
':input[name="exposed_form_options[bef][general][allow_secondary]"]' => ['checked' => TRUE],
],
],
'#description' => $this->t('Places this element in the secondary options portion of the exposed form.'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
$is_secondary = !empty($form['secondary']) && $this->configuration['advanced']['is_secondary'];
foreach ($this->pagerElements as $element) {
// Sanity check to make sure the element exists.
if (empty($form[$element])) {
continue;
}
if ($is_secondary) {
$this->addElementToGroup($form, $form_state, $element, 'secondary');
}
// Finally, add some metadata to the form element.
$this->addContext($form[$element]);
}
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\pager;
use Drupal\Core\Form\FormStateInterface;
/**
* Radio Buttons pager widget implementation.
*
* @BetterExposedFiltersPagerWidget(
* id = "bef",
* label = @Translation("Radio Buttons"),
* )
*/
class RadioButtons extends PagerWidgetBase {
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
parent::exposedFormAlter($form, $form_state);
if (!empty($form['items_per_page'])) {
$form['items_per_page']['#type'] = 'radios';
$form['items_per_page']['#prefix'] = '<div class="bef-sortby bef-select-as-radios">';
$form['items_per_page']['#suffix'] = '</div>';
}
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\sort;
use Drupal\Core\Form\FormStateInterface;
/**
* Default widget implementation.
*
* @BetterExposedFiltersSortWidget(
* id = "default",
* label = @Translation("Default"),
* )
*/
class DefaultWidget extends SortWidgetBase {
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
parent::exposedFormAlter($form, $form_state);
foreach ($this->sortElements as $element) {
if (!empty($form[$element])) {
$form[$element]['#type'] = 'select';
}
}
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\sort;
use Drupal\Core\Form\FormStateInterface;
/**
* Radio Buttons sort widget implementation.
*
* @BetterExposedFiltersSortWidget(
* id = "bef_links",
* label = @Translation("Links"),
* )
*/
class Links extends SortWidgetBase {
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
parent::exposedFormAlter($form, $form_state);
foreach ($this->sortElements as $element) {
if (!empty($form[$element])) {
$form[$element]['#theme'] = 'bef_links';
// Exposed form displayed as blocks can appear on pages other than
// the view results appear on. This can cause problems with
// select_as_links options as they will use the wrong path. We
// provide a hint for theme functions to correct this.
$form[$element]['#bef_path'] = $this->getExposedFormActionUrl($form_state);
}
}
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\sort;
use Drupal\Core\Form\FormStateInterface;
/**
* Radio Buttons sort widget implementation.
*
* @BetterExposedFiltersSortWidget(
* id = "bef",
* label = @Translation("Radio Buttons"),
* )
*/
class RadioButtons extends SortWidgetBase {
/**
* {@inheritdoc}
*/
public function exposedFormAlter(array &$form, FormStateInterface $form_state) {
parent::exposedFormAlter($form, $form_state);
foreach ($this->sortElements as $element) {
if (!empty($form[$element])) {
$form[$element]['#theme'] = 'bef_radios';
$form[$element]['#type'] = 'radios';
}
}
}
}
{#
Themes Views' default multi-select element as a set of checkboxes.
Available variables:
- wrapper_attributes: attributes for the wrapper element.
- element: The collection of checkboxes.
- children: An array of keys for the children of element.
- is_nested: TRUE if this is to be rendered as a nested list.
- depth: If is_nested is TRUE, this holds an array in the form of
child_id => nesting_level which defines the depth a given element should
appear in the nested list.
#}
{% set classes = [
'form-checkboxes',
is_nested ? 'bef-nested',
show_select_all_none ? 'bef-select-all-none',
show_select_all_none_nested ? 'bef-select-all-none-nested',
display_inline ? 'form--inline'
] %}
<div{{ wrapper_attributes.addClass(classes) }}>
{% set current_nesting_level = 0 %}
{% for child in children %}
{% set item = attribute(element, child) %}
{% if is_nested %}
{% set new_nesting_level = attribute(depth, child) %}
{% include '@better_exposed_filters/bef-nested-elements.html.twig' %}
{% set current_nesting_level = new_nesting_level %}
{% else %}
{{ item }}
{% endif %}
{% endfor %}
</div>
{#
Themes Views' default multi-select element as hidden input elements.
Available variables:
- hidden_elements: Array of hidden input elements for all possible filter
options.
- selected: Array of selected values.
#}
{% for value, hidden in hidden_elements %}
{% if value in selected %}
{{ hidden }}
{% endif %}
{% endfor %}
{%
set classes = [
'bef-links',
is_nested ? 'bef-nested'
]
%}
{%
set is_nested = true
%}
<div{{ attributes.addClass(classes) }}>
{% set current_nesting_level = 0 %}
{% for child in children %}
{% set item = attribute(element, child) %}
{% set new_nesting_level = attribute(depth, child) %}
{% include '@better_exposed_filters/bef-nested-elements.html.twig' %}
{% set current_nesting_level = new_nesting_level %}
{% endfor %}
</div>
{#
Themes hierarchical taxonomy terms as nested <ul> elements.
This template is intended to be called from within another template to provide
the "scaffolding" of nested lists while the calling template provides the
actual filter element via the 'item' variable.
Available variables:
- current_nesting_level: the nesting level of the most recently printe item.
- new_nesting_level: the nesting level of the item to print.
- item: The item to print.
- loop: The loop variable from the iterator that calls this template.
Requires the loop.first and loop.last elements.
#}
{% apply spaceless %}
{% set delta = (current_nesting_level - new_nesting_level) | abs %}
{% if loop.first %}
<ul>
{% else %}
{% if delta %}
{% for i in 1..delta %}
{% if new_nesting_level > current_nesting_level %}
<ul>
{% else %}
</ul>
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
<li>{{ item }}
{% if loop.last %}
{# Close any remaining <li> tags #}
{% for i in new_nesting_level..0 %}
</li></ul>
{% endfor %}
{% endif %}
{% endapply %}
{#
Themes a single-select exposed form element as radio buttons.
Available variables:
- wrapper_attributes: attributes for the wrapper element.
- element: The collection of checkboxes.
- children: An array of keys for the children of element.
- is_nested: TRUE if this is to be rendered as a nested list.
- depth: If is_nested is TRUE, this holds an array in the form of
child_id => nesting_level which defines the depth a given element should
appear in the nested list.
#}
{%
set classes = [
'form-radios',
is_nested ? 'bef-nested',
display_inline ? 'form--inline'
]
%}
<div{{ wrapper_attributes.addClass(classes) }}>
{% set current_nesting_level = 0 %}
{% for child in children %}
{% set item = attribute(element, child) %}
{% if is_nested %}
{% set new_nesting_level = attribute(depth, child) %}
{% include '@better_exposed_filters/bef-nested-elements.html.twig' %}
{% set current_nesting_level = new_nesting_level %}
{% else %}
{{ item }}
{% endif %}
{% endfor %}
</div>
name: BEF Test
type: module
description: Test module for Better Exposed Filters
core_version_requirement: ^8.8 || ^9
package: Views
dependencies:
- better_exposed_filters:better_exposed_filters
- drupal:datetime
- drupal:node
- drupal:options
- drupal:user
# Information added by Drupal.org packaging script on 2020-07-07
version: '8.x-5.0-beta1'
project: 'better_exposed_filters'
datestamp: 1594141894
<?php
/**
* @file
* Provides install hooks for the BEF Test module.
*/
/**
* Adds terms to the hierarchical "location" vocabulary.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* Thrown if the entity type doesn't exist.
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* Thrown if the storage handler couldn't be loaded.
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures an exception is thrown.
*/
function bef_test_install() {
// Set up an example hierarchical terms in the "Location" vocab.
$locations = [
'United States' => [
'California' => [
'San Francisco',
'San Diego',
'Santa Barbara',
],
'Oregon' => [
'Portland',
'Eugene',
],
'Washington' => [
'Seattle',
'Spokane',
'Walla Walla',
],
],
'Canada' => [
'British Columbia' => [
'Vancouver',
'Victoria',
'Whistler',
],
'Alberta' => [
'Calgary',
'Edmonton',
'Lake Louise',
],
],
'Mexico' => [],
];
foreach ($locations as $country => $states) {
$country_tid = _bef_test_add_term($country);
if ($country_tid && !empty($states)) {
foreach ($states as $state => $cities) {
$state_tid = _bef_test_add_term($state, $country_tid);
if ($state_tid && !empty($cities)) {
foreach ($cities as $city) {
_bef_test_add_term($city, $state_tid);
}
}
}
}
}
}
/**
* Adds a new term to the bef_test-location vocabulary.
*
* If a TID is specified in $parent, the new term is added as a child of that
* term.
*
* @param string $name
* The name of the new term.
* @param int $parent
* The (optional) TID of the parent term.
*
* @return int
* TID of the newly created term.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* Thrown if the entity type doesn't exist.
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* Thrown if the storage handler couldn't be loaded.
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures an exception is thrown.
*/
function _bef_test_add_term($name, $parent = 0) {
$term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->create([
'vid' => 'bef_test_location',
'name' => $name,
'parent' => [$parent],
]);
$term->save();
return $term->id();
}
<?php
/**
* @file
* Contains bef_test.module..
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function bef_test_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the bef_test module.
case 'help.page.bef_test':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Test module for Better Exposed Filters') . '</p>';
return $output;
default:
}
}
langcode: en
status: true
dependencies:
config:
- field.field.node.bef_test.body
- field.field.node.bef_test.field_bef_boolean
- field.field.node.bef_test.field_bef_email
- field.field.node.bef_test.field_bef_integer
- field.field.node.bef_test.field_bef_letters
- field.field.node.bef_test.field_bef_location
- node.type.bef_test
module:
- path
enforced:
module:
- bef_test
id: node.bef_test.default
targetEntityType: node
bundle: bef_test
mode: default
content:
created:
type: datetime_timestamp
weight: 10
settings: { }
third_party_settings: { }
field_bef_boolean:
weight: 31
settings:
display_label: false
third_party_settings: { }
type: boolean_checkbox
field_bef_email:
weight: 33
settings:
placeholder: ''
third_party_settings: { }
type: email_default
field_bef_integer:
weight: 34
settings: { }
third_party_settings: { }
type: options_select
field_bef_letters:
weight: 35
settings: { }
third_party_settings: { }
type: options_select
field_bef_location:
weight: 36
settings:
match_operator: CONTAINS
size: 60
placeholder: ''
third_party_settings: { }
type: entity_reference_autocomplete
path:
type: path
weight: 30
settings: { }
third_party_settings: { }
promote:
type: boolean_checkbox
settings:
display_label: true
weight: 15
third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
display_label: true
weight: 16
third_party_settings: { }
title:
type: string_textfield
weight: -5
settings:
size: 60
placeholder: ''
third_party_settings: { }
uid:
type: entity_reference_autocomplete
weight: 5
settings:
match_operator: CONTAINS
size: 60
placeholder: ''
third_party_settings: { }
hidden:
body: true
langcode: en
status: true
dependencies:
config:
- field.field.node.bef_test.body
- field.field.node.bef_test.field_bef_boolean
- field.field.node.bef_test.field_bef_email
- field.field.node.bef_test.field_bef_integer
- field.field.node.bef_test.field_bef_letters
- field.field.node.bef_test.field_bef_location
- node.type.bef_test
module:
- user
enforced:
module:
- bef_test
id: node.bef_test.default
targetEntityType: node
bundle: bef_test
mode: default
content:
field_bef_location:
weight: 101
label: above
settings:
link: true
third_party_settings: { }
type: entity_reference_label
links:
weight: 100
hidden:
body: true
field_bef_boolean: true
field_bef_email: true
field_bef_integer: true
field_bef_letters: true
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.teaser
- node.type.bef_test
module:
- user
enforced:
module:
- bef_test
id: node.bef_test.teaser
targetEntityType: node
bundle: bef_test
mode: teaser
content:
links:
weight: 100
hidden: { }
langcode: en
status: true
dependencies:
config:
- field.storage.node.body
- node.type.bef_test
module:
- text
enforced:
module:
- bef_test
id: node.bef_test.body
field_name: body
entity_type: node
bundle: bef_test
label: Body
description: ''
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
display_summary: true
field_type: text_with_summary
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_bef_boolean
- node.type.bef_test
enforced:
module:
- bef_test
id: node.bef_test.field_bef_boolean
field_name: field_bef_boolean
entity_type: node
bundle: bef_test
label: bef_boolean
description: ''
required: false
translatable: false
default_value:
-
value: 0
default_value_callback: ''
settings:
on_label: 'On'
off_label: 'Off'
field_type: boolean
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_bef_email
- node.type.bef_test
enforced:
module:
- bef_test
id: node.bef_test.field_bef_email
field_name: field_bef_email
entity_type: node
bundle: bef_test
label: bef_email
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: email
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_bef_integer
- node.type.bef_test
module:
- options
enforced:
module:
- bef_test
id: node.bef_test.field_bef_integer
field_name: field_bef_integer
entity_type: node
bundle: bef_test
label: bef_integer
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: list_integer
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_bef_letters
- node.type.bef_test
module:
- options
enforced:
module:
- bef_test
id: node.bef_test.field_bef_letters
field_name: field_bef_letters
entity_type: node
bundle: bef_test
label: bef_letters
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: list_string
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_bef_location
- node.type.bef_test
- taxonomy.vocabulary.bef_test_location
enforced:
module:
- bef_test
id: node.bef_test.field_bef_location
field_name: field_bef_location
entity_type: node
bundle: bef_test
label: bef_location
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:taxonomy_term'
handler_settings:
target_bundles:
bef_test_location: bef_test_location
sort:
field: _none
auto_create: false
field_type: entity_reference
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_bef_price
- node.type.bef_test
enforced:
module:
- bef_test
id: node.bef_test.field_bef_price
field_name: field_bef_price
entity_type: node
bundle: bef_test
label: Price
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
min: !!float 0
max: !!float 10000
prefix: $
suffix: ''
field_type: decimal
langcode: en
status: true
dependencies:
module:
- node
enforced:
module:
- bef_test
id: node.field_bef_boolean
field_name: field_bef_boolean
entity_type: node
type: boolean
settings: { }
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
langcode: en
status: true
dependencies:
module:
- node
enforced:
module:
- bef_test
id: node.field_bef_email
field_name: field_bef_email
entity_type: node
type: email
settings: { }
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
langcode: en
status: true
dependencies:
module:
- node
- options
enforced:
module:
- bef_test
id: node.field_bef_integer
field_name: field_bef_integer
entity_type: node
type: list_integer
settings:
allowed_values:
-
value: 1
label: One
-
value: 2
label: Two
-
value: 3
label: Three
-
value: 4
label: Four
-
value: 5
label: Five
allowed_values_function: ''
module: options
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
langcode: en
status: true
dependencies:
module:
- node
- options
enforced:
module:
- bef_test
id: node.field_bef_letters
field_name: field_bef_letters
entity_type: node
type: list_string
settings:
allowed_values:
-
value: a
label: Aardvark
-
value: b
label: 'Bumble & the Bee'
-
value: c
label: 'Le Chimpanzé'
-
value: d
label: Donkey
-
value: e
label: Elephant
allowed_values_function: ''
module: options
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
langcode: en
status: true
dependencies:
module:
- node
- taxonomy
enforced:
module:
- bef_test
id: node.field_bef_location
field_name: field_bef_location
entity_type: node
type: entity_reference
settings:
target_type: taxonomy_term
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
langcode: en
status: true
dependencies:
module:
- node
enforced:
module:
- bef_test
id: node.field_bef_price
field_name: field_bef_price
entity_type: node
type: decimal
settings:
precision: 10
scale: 2
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
langcode: en
status: true
dependencies:
enforced:
module:
- bef_test
name: 'BEF Test'
type: bef_test
description: 'Test content type for the Better Exposed Filters module.'
help: ''
new_revision: false
preview_mode: 1
display_submitted: true
langcode: en
status: true
dependencies:
enforced:
module:
- bef_test
name: 'BEF Test Location'
vid: bef_test_location
description: 'Hierarchical vocabulary for testing Better Exposed Filters'
hierarchy: 0
weight: 0
<?php
namespace Drupal\Tests\better_exposed_filters\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\better_exposed_filters\Traits\BetterExposedFiltersTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\views\Views;
/**
* Tests the basic AJAX functionality of BEF exposed forms.
*
* @group better_exposed_filters
*/
class BetterExposedFiltersTest extends WebDriverTestBase {
use BetterExposedFiltersTrait;
use ContentTypeCreationTrait;
use NodeCreationTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
public static $modules = [
'block',
'node',
'views',
'taxonomy',
'better_exposed_filters',
'bef_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Enable AJAX on the our test view.
\Drupal::configFactory()->getEditable('views.view.bef_test')
->set('display.default.display_options.use_ajax', TRUE)
->save();
// Create a few test nodes.
$this->createNode([
'title' => 'Page One',
'field_bef_boolean' => '',
'field_bef_email' => 'bef-test@drupal.org',
'field_bef_integer' => '1',
'field_bef_letters' => 'Aardvark',
// Seattle.
'field_bef_location' => '10',
'type' => 'bef_test',
]);
$this->createNode([
'title' => 'Page Two',
'field_bef_boolean' => '',
'field_bef_email' => 'bef-test@drupal.org',
'field_bef_integer' => '2',
'field_bef_letters' => 'Bumble & the Bee',
// Vancouver.
'field_bef_location' => '15',
'type' => 'bef_test',
]);
}
/**
* Tests if filtering via auto-submit works.
*/
public function testAutoSubmit() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Enable auto-submit, but disable for text fields.
$this->setBetterExposedOptions($view, [
'general' => [
'autosubmit' => TRUE,
'autosubmit_exclude_textfield' => TRUE,
],
]);
// Visit the bef-test page.
$this->drupalGet('bef-test');
$session = $this->getSession();
$page = $session->getPage();
// Ensure that the content we're testing for is present.
$html = $page->getHtml();
$this->assertStringContainsString('Page One', $html);
$this->assertStringContainsString('Page Two', $html);
// Search for "Page One".
$field_bef_integer = $page->findField('field_bef_integer_value');
$field_bef_integer->setValue('1');
$field_bef_integer->blur();
$this->assertSession()->assertWaitOnAjaxRequest();
// Verify that only the "Page One" Node is present.
$html = $page->getHtml();
$this->assertStringContainsString('Page One', $html);
$this->assertStringNotContainsString('Page Two', $html);
// Enter value in email field.
$field_bef_email = $page->find('css', '.form-item-field-bef-email-value input');
$field_bef_email->setValue('qwerty@test.com');
$this->assertSession()->assertWaitOnAjaxRequest();
// Verify nothing has changed.
$this->assertStringContainsString('Page One', $html);
$this->assertStringNotContainsString('Page Two', $html);
// Submit form.
$this->submitForm([], 'Apply');
$this->assertSession()->assertWaitOnAjaxRequest();
// Verify no results are visible.
$html = $page->getHtml();
$this->assertStringNotContainsString('Page One', $html);
$this->assertStringNotContainsString('Page Two', $html);
}
/**
* Tests if filtering via auto-submit works if exposed form is a block.
*/
public function testAutoSubmitWithExposedFormBlock() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
$block = $this->drupalPlaceBlock('views_exposed_filter_block:bef_test-page_2');
// Enable auto-submit, but disable for text fields.
$this->setBetterExposedOptions($view, [
'general' => [
'autosubmit' => TRUE,
'autosubmit_exclude_textfield' => TRUE,
],
]);
// Visit the bef-test page.
$this->drupalGet('bef-test-with-block');
$session = $this->getSession();
$page = $session->getPage();
// Ensure that the content we're testing for is present.
$html = $page->getHtml();
$this->assertStringContainsString('Page One', $html);
$this->assertStringContainsString('Page Two', $html);
// Search for "Page One".
$field_bef_integer = $page->findField('field_bef_integer_value');
$field_bef_integer->setValue('1');
$field_bef_integer->blur();
$this->assertSession()->assertWaitOnAjaxRequest();
// Verify that only the "Page One" Node is present.
$html = $page->getHtml();
$this->assertStringContainsString('Page One', $html);
$this->assertStringNotContainsString('Page Two', $html);
// Enter value in email field.
$field_bef_email = $page->find('css', '.form-item-field-bef-email-value input');
$field_bef_email->setValue('qwerty@test.com');
$this->assertSession()->assertWaitOnAjaxRequest();
// Verify nothing has changed.
$this->assertStringContainsString('Page One', $html);
$this->assertStringNotContainsString('Page Two', $html);
// Submit form.
$this->submitForm([], 'Apply');
$this->assertSession()->assertWaitOnAjaxRequest();
// Verify no results are visible.
$html = $page->getHtml();
$this->assertStringNotContainsString('Page One', $html);
$this->assertStringNotContainsString('Page Two', $html);
}
/**
* Tests placing exposed filters inside a collapsible field-set.
*/
public function testSecondaryOptions() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Enable auto-submit, but disable for text fields.
$this->setBetterExposedOptions($view, [
'general' => [
'allow_secondary' => TRUE,
'secondary_label' => 'Secondary Options TEST',
],
'sort' => [
'plugin_id' => 'default',
'advanced' => [
'is_secondary' => TRUE,
],
],
'pager' => [
'plugin_id' => 'default',
'advanced' => [
'is_secondary' => TRUE,
],
],
'filter' => [
'field_bef_boolean_value' => [
'plugin_id' => 'default',
'advanced' => [
'is_secondary' => TRUE,
],
],
],
]);
// Visit the bef-test page.
$this->drupalGet('bef-test');
$session = $this->getSession();
$page = $session->getPage();
// Assert our fields are initially hidden inside the collapsible field-set.
$secondary_options = $page->find('css', '.bef--secondary');
$this->assertFalse($secondary_options->hasAttribute('open'));
$secondary_options->hasField('field_bef_boolean_value');
// Submit form and set a value for the boolean field.
$secondary_options->click();
$this->submitForm(['field_bef_boolean_value' => 1], 'Apply');
$session = $this->getSession();
$page = $session->getPage();
// Verify our field-set is open and our fields visible.
$secondary_options = $page->find('css', '.bef--secondary');
$this->assertTrue($secondary_options->hasAttribute('open'));
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel;
use Drupal\Tests\better_exposed_filters\Kernel\BetterExposedFiltersKernelTestBase;
use Drupal\views\Views;
/**
* Tests the radio buttons/checkboxes filter widget (i.e. "bef").
*
* @group better_exposed_filters
*
* @see \Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter\RadioButtons
*/
class BetterExposedFiltersKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests hiding the submit button when auto-submit is enabled.
*/
public function testHideSubmitButtonOnAutoSubmit() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Enable auto-submit and hide auto-submit button.
$this->setBetterExposedOptions($view, [
'general' => [
'autosubmit' => TRUE,
'autosubmit_hide' => TRUE,
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our "submit" button is hidden.
$actual = $this->xpath("//form//input[@type='submit'][contains(concat(' ',normalize-space(@class),' '),' js-hide ')]");
$this->assertCount(1, $actual, 'Submit button was hidden successfully.');
$view->destroy();
}
/**
* Tests moving sorts, filters and pager options into secondary fieldset.
*/
public function testSecondaryOptions() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Enable secondary options and set label.
$this->setBetterExposedOptions($view, [
'general' => [
'allow_secondary' => TRUE,
'secondary_label' => 'Secondary Options TEST',
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Assert our "secondary" options detail is hidden if no fields are placed.
$actual = $this->xpath("//form//details[@data-drupal-selector='edit-secondary']");
$this->assertCount(0, $actual, 'Secondary options are hidden because no fields were placed.');
$view->destroy();
// Move sort, pager and "field_bef_boolean" into secondary options.
$view = Views::getView('bef_test');
$this->setBetterExposedOptions($view, [
'general' => [
'allow_secondary' => TRUE,
'secondary_label' => 'Secondary Options TEST',
],
'sort' => [
'plugin_id' => 'default',
'advanced' => [
'is_secondary' => TRUE,
],
],
'pager' => [
'plugin_id' => 'default',
'advanced' => [
'is_secondary' => TRUE,
],
],
'filter' => [
'field_bef_boolean_value' => [
'plugin_id' => 'default',
'advanced' => [
'is_secondary' => TRUE,
],
],
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Assert our "secondary" options detail is visible.
$actual = $this->xpath("//form//details[@data-drupal-selector='edit-secondary']");
$this->assertCount(1, $actual, 'Secondary options is visible.');
// Assert sort option was placed in secondary details.
$actual = $this->xpath("//form//details[@data-drupal-selector='edit-secondary']//select[@name='sort_by']");
$this->assertCount(1, $actual, 'Exposed sort was placed in secondary fieldset.');
// Assert pager option was placed in secondary details.
$actual = $this->xpath("//form//details[@data-drupal-selector='edit-secondary']//select[@name='items_per_page']");
$this->assertCount(1, $actual, 'Exposed pager was placed in secondary fieldset.');
// Assert filter option was placed in secondary details.
$actual = $this->xpath("//form//details[@data-drupal-selector='edit-secondary']//select[@name='field_bef_boolean_value']");
$this->assertCount(1, $actual, 'Exposed filter "field_bef_boolean" was placed in secondary fieldset.');
$view->destroy();
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel;
use Drupal\Tests\better_exposed_filters\Traits\BetterExposedFiltersTrait;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
use Drupal\views\ViewExecutable;
/**
* Defines a base class for Better Exposed Filters kernel testing.
*/
abstract class BetterExposedFiltersKernelTestBase extends ViewsKernelTestBase {
use BetterExposedFiltersTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'system',
'field',
'views',
'node',
'filter',
'options',
'text',
'taxonomy',
'better_exposed_filters',
'bef_test',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE) {
parent::setUp();
$this->installSchema('node', ['node_access']);
$this->installEntitySchema('node');
$this->installEntitySchema('taxonomy_vocabulary');
$this->installEntitySchema('taxonomy_term');
\Drupal::moduleHandler()->loadInclude('bef_test', 'install');
bef_test_install();
$this->installConfig(['system', 'field', 'node', 'taxonomy', 'bef_test']);
}
/**
* Gets the render array for the views exposed form.
*
* @param \Drupal\views\ViewExecutable $view
* The view object.
*
* @return array
* The render array.
*/
public function getExposedFormRenderArray(ViewExecutable $view) {
$this->executeView($view);
$exposed_form = $view->display_handler->getPlugin('exposed_form');
return $exposed_form->renderExposedForm();
}
/**
* Renders the views exposed form.
*
* @param \Drupal\views\ViewExecutable $view
* The view object.
*/
protected function renderExposedForm(ViewExecutable $view) {
$output = $this->getExposedFormRenderArray($view);
$this->setRawContent(\Drupal::service('renderer')->renderRoot($output));
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel\Plugin\filter;
use Drupal\Tests\better_exposed_filters\Kernel\BetterExposedFiltersKernelTestBase;
use Drupal\views\Views;
/**
* Tests the advanced options of a filter widget.
*
* @group better_exposed_filters
*
* @see \Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter\FilterWidgetBase
*/
class FilterWidgetKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests grouping filter options.
*
* There is a bug in views where changing the identifier of an exposed
* grouped filter will cause an undefined index notice.
*
* @todo Enable test once https://www.drupal.org/project/drupal/issues/2884296
* is fixed
*/
/*public function testGroupedFilters() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Ensure our filter "field_bef_boolean_value" is grouped.
$display['display_options']['filters']['field_bef_boolean_value']['is_grouped'] = TRUE;
$display['display_options']['filters']['field_bef_boolean_value']['group_info'] = [
'plugin_id' => 'boolean',
'label' => 'bef_boolean (field_bef_boolean)',
'description' => '',
'identifier' => 'field_bef_boolean_value2',
'optional' => TRUE,
'widget' => 'select',
'multiple' => FALSE,
'remember' => FALSE,
'default_group' => 'All',
'default_group_multiple' => [],
'group_items' => [
1 => [
'title' => 'YES',
'operator' => '=',
'value' => '1',
],
2 => [
'title' => 'NO',
'operator' => '=',
'value' => '0',
],
],
];
// Render the exposed form.
$output = $this->getExposedFormRenderArray($view);
// Check our "FIELD_BEF_BOOLEAN" filter is rendered with id
// "field_bef_boolean_value2".
$this->assertTrue(isset($output['field_bef_boolean_value2']), 'Exposed filter "FIELD_BEF_BOOLEAN" is exposed with id "field_bef_boolean_value2".');
$view->destroy();
}*/
/**
* Tests sorting filter options alphabetically.
*/
public function testSortFilterOptions() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Get the exposed form render array.
$output = $this->getExposedFormRenderArray($view);
// Assert our "field_bef_integer" filter options are not sorted
// alphabetically, but by key.
$sorted_options = $options = $output['field_bef_integer_value']['#options'];
asort($sorted_options);
$this->assertNotEqual(array_keys($options), array_keys($sorted_options), '"Field BEF integer" options are not sorted alphabetically.');
$view->destroy();
// Enable sort for filter options.
$this->setBetterExposedOptions($view, [
'filter' => [
'field_bef_integer_value' => [
'plugin_id' => 'default',
'advanced' => [
'sort_options' => TRUE,
],
],
],
]);
// Get the exposed form render array.
$output = $this->getExposedFormRenderArray($view);
// Assert our "field_bef_integer" filter options are sorted alphabetically.
$sorted_options = $options = $output['field_bef_integer_value']['#options'];
asort($sorted_options);
// Assert our "collapsible" options detail is visible.
$this->assertEqual(array_keys($options), array_keys($sorted_options), '"Field BEF integer" options are sorted alphabetically.');
$view->destroy();
}
/**
* Tests moving filter option into collapsible fieldset.
*/
public function testCollapsibleOption() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Enable collapsible options.
$this->setBetterExposedOptions($view, [
'filter' => [
'field_bef_email_value' => [
'plugin_id' => 'default',
'advanced' => [
'collapsible' => TRUE,
],
],
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Assert our "collapsible" options detail is visible.
$actual = $this->xpath("//form//details[@data-drupal-selector='edit-field-bef-email-value-collapsible']");
$this->assertCount(1, $actual, '"Field BEF Email" option is displayed as collapsible fieldset.');
$view->destroy();
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel\Plugin\filter;
use Drupal\Tests\better_exposed_filters\Kernel\BetterExposedFiltersKernelTestBase;
use Drupal\views\Views;
/**
* Tests the options of a hidden filter widget.
*
* @group better_exposed_filters
*
* @see \Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter\FilterWidgetBase
*/
class HiddenFilterWidgetKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests hiding element with single option.
*/
public function testSingleExposedHiddenElement() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Change exposed filter "field_bef_boolean" to hidden (i.e. 'bef_hidden').
$this->setBetterExposedOptions($view, [
'filter' => [
'field_bef_boolean_value' => [
'plugin_id' => 'bef_hidden',
],
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our "FIELD_BEF_BOOLEAN" filter is rendered as a hidden element.
$actual = $this->xpath('//form//input[@type="hidden" and starts-with(@name, "field_bef_boolean_value")]');
$this->assertCount(1, $actual, 'Exposed filter "FIELD_BEF_BOOLEAN" is hidden.');
$view->destroy();
}
/**
* Tests hiding element with multiple options.
*/
public function testMultipleExposedHiddenElement() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Set filter to "multiple".
$display['display_options']['filters']['field_bef_integer_value']['expose']['multiple'] = TRUE;
// Change exposed filter "field_bef_integer" to hidden (i.e. 'bef_hidden').
$this->setBetterExposedOptions($view, [
'filter' => [
'field_bef_integer_value' => [
'plugin_id' => 'bef_hidden',
],
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our "FIELD_BEF_INTEGER" filter is rendered as a hidden element.
$actual = $this->xpath('//form//label[@type="label" and starts-with(@for, "edit-field-bef-integer-value")]');
$this->assertCount(0, $actual, 'Exposed filter "FIELD_BEF_INTEGER" is hidden.');
$actual = $this->xpath('//form//input[@type="hidden" and starts-with(@name, "field_bef_integer_value")]');
$this->assertCount(0, $actual, 'Exposed filter "FIELD_BEF_INTEGER" has no selected values.');
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel\Plugin\filter;
use Drupal\Tests\better_exposed_filters\Kernel\BetterExposedFiltersKernelTestBase;
use Drupal\views\Views;
/**
* Tests the links filter widget (i.e. "bef_links").
*
* @group better_exposed_filters
*
* @see \Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter\Links
*/
class LinksFilterWidgetKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests the exposed links filter widget.
*/
public function testExposedLinks() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Ensure our filter "term_node_tid_depth" has show hierarchy enabled.
$display['display_options']['filters']['term_node_tid_depth']['hierarchy'] = TRUE;
// Change exposed filter "field_bef_integer" and "term_node_tid_depth" to
// links (i.e. 'bef_links').
$this->setBetterExposedOptions($view, [
'filter' => [
'field_bef_integer_value' => [
'plugin_id' => 'bef_links',
],
'term_node_tid_depth' => [
'plugin_id' => 'bef_links',
],
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our "FIELD_BEF_INTEGER" filter is rendered as links.
$actual = $this->xpath('//form//a[starts-with(@name, "field_bef_integer_value")]');
$this->assertCount(6, $actual, 'Exposed filter "FIELD_BEF_INTEGER" has correct number of exposed links.');
// Check our "TERM_NODE_TID_DEPTH" filter is rendered as nested links.
$actual = $this->xpath("//form//div[contains(concat(' ',normalize-space(@class),' '),' bef-nested ')]");
$this->assertCount(1, $actual, 'Exposed filter "TERM_NODE_TID_DEPTH" has bef-nested class');
$actual = $this->xpath('//form//div[@id="edit-term-node-tid-depth--2"]/ul/li/a[starts-with(@name, "term_node_tid_depth")]');
$this->assertCount(4, $actual, 'Exposed filter "TERM_NODE_TID_DEPTH" has correct number of exposed top-level links.');
$actual = $this->xpath('//form//div[@id="edit-term-node-tid-depth--2"]/ul/li/ul/li/a[starts-with(@name, "term_node_tid_depth")]');
$this->assertCount(5, $actual, 'Exposed filter "TERM_NODE_TID_DEPTH" has correct number of exposed second-level links.');
$actual = $this->xpath('//form//div[@id="edit-term-node-tid-depth--2"]/ul/li/ul/li/ul/li/a[starts-with(@name, "term_node_tid_depth")]');
$this->assertCount(14, $actual, 'Exposed filter "TERM_NODE_TID_DEPTH" has correct number of exposed third-level links.');
$view->destroy();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment