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
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
-- 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
<?php
/**
* @file
* Theme hooks, preprocessor, and suggestions.
*/
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Element;
use Drupal\Core\Template\Attribute;
/**
* Implements hook_theme().
*/
function better_exposed_filters_theme($existing, $type, $theme, $path) {
return [
'bef_checkboxes' => [
'render element' => 'element',
],
'bef_radios' => [
'render element' => 'element',
],
'bef_links' => [
'render element' => 'element',
],
'bef_hidden' => [
'render element' => 'element',
],
];
}
/**
* Implements hook_theme_suggestions_alter().
*/
function better_exposed_filters_theme_suggestions_alter(array &$suggestions, array $variables, $hook) {
// Target bef elements.
if ($hook === 'form_element') {
$plugin_type = $variables['element']['#context']['plugin_type'] ?? FALSE;
if ($plugin_type && $plugin_type === 'bef') {
$view_id = $variables['element']['#context']['#view_id'];
$display_id = $variables['element']['#context']['#display_id'];
if ($view_id) {
$suggestions[] = $hook . '__' . $view_id;
if ($display_id) {
$suggestions[] = $hook . '__' . $view_id . '__' . $display_id;
}
}
}
}
}
/**
* Prepares variables for views exposed form templates.
*
* Default template: views-exposed-form.html.twig.
*
* @param array $variables
* An associative array containing:
* - form: A render element representing the form.
*/
function better_exposed_filters_preprocess_views_exposed_form(array &$variables) {
// Checks if Token module is enabled.
if (!\Drupal::moduleHandler()->moduleExists('token')) {
return;
}
// Replaces tokens in description field of the exposed filter.
foreach ($variables['form']['#info'] as $name => &$info) {
if (isset($info['description']) && isset($variables['form'][explode('filter-', $name)[1]]['#description'])) {
$info['description'] = \Drupal::service('token')->replace($info['description']);
$variables['form'][explode('filter-', $name)[1]]['#description'] = $info['description'];
}
}
}
/******************************************************************************
* Preprocess functions for BEF themed elements.
******************************************************************************/
/**
* Prepares variables for bef-checkboxes template.
*
* Default template: bef-checkboxes.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the exposed form element.
*/
function template_preprocess_bef_checkboxes(array &$variables) {
$element = &$variables['element'];
// Create new wrapper attributes since the element attributes will be used
// on the fieldset (@see template_preprocess_fieldset).
$variables['wrapper_attributes'] = new Attribute();
$variables['children'] = Element::children($element);
$variables['show_select_all_none'] = $element['#bef_select_all_none'] ?? FALSE;
$variables['show_select_all_none_nested'] = $element['#bef_select_all_none_nested'] ?? FALSE;
$variables['display_inline'] = $element['#bef_display_inline'] ?? FALSE;
// Set element name.
$variables['attributes']['name'] = $element['#name'];
// Handle nested checkboxes.
if (!empty($variables['element']['#bef_nested'])) {
_bef_preprocess_nested_elements($variables);
}
}
/**
* Prepares variables for bef-radios template.
*
* Default template: bef-radios.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the exposed form element.
*/
function template_preprocess_bef_radios(array &$variables) {
$element = &$variables['element'];
// Create new wrapper attributes since the element attributes will be used
// on the fieldset (@see template_preprocess_fieldset).
$variables['wrapper_attributes'] = new Attribute();
$variables['children'] = Element::children($element);
$variables['display_inline'] = $element['#bef_display_inline'] ?? FALSE;
// Set element name.
$variables['attributes']['name'] = $element['#name'];
// Handle nested radio buttons.
if (!empty($variables['element']['#bef_nested'])) {
_bef_preprocess_nested_elements($variables);
}
}
/**
* Prepares variables for bef-hidden template.
*
* Default template: bef-hidden.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the exposed form element.
*/
function template_preprocess_bef_hidden(array &$variables) {
$element = $variables['element'];
// This theme function is only used for multi-select elements.
$variables['is_multiple'] = TRUE;
$variables['selected'] = empty($element['#value']) ? $element['#default_value'] : $element['#value'];
$variables['hidden_elements'] = [];
foreach ($element['#options'] as $value => $label) {
$variables['hidden_elements'][$value] = [
'#type' => 'hidden',
'#value' => $value,
'#name' => $element['#name'] . '[]',
];
}
// @todo:
// Check for optgroups. Put subelements in the $element_set array and add a
// group heading. Otherwise, just add the element to the set.
// $element_set = array();
// if (is_array($elem)) {
// $element_set = $elem;
// }
// else {
// $element_set[$option] = $elem;
// }
}
/**
* Prepares variables for bef-links template.
*
* Default template: bef-links.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the exposed form element.
*/
function template_preprocess_bef_links(array &$variables) {
// Collect some variables before we start tweaking the element.
$element = &$variables['element'];
$options = $element['#options'];
$name = $element['#name'];
// Set element name.
$variables['attributes']['name'] = $name;
// Get the query string arguments from the current request.
$existing_query = \Drupal::service('request_stack')->getCurrentRequest()->query->all();
// Remove page parameter from query.
unset($existing_query['page']);
// Store selected values.
$selectedValues = $element['#value'];
if (!is_array($selectedValues)) {
$selectedValues = [$selectedValues];
}
$variables['links'] = [];
foreach ($options as $optionValue => $optionLabel) {
// Build a new Url object for each link since the query string changes with
// each option.
/** @var Drupal\Core\Url $url */
$url = clone($element['#bef_path']);
// Allow visitors to toggle a filter setting on and off. This is not as
// simple as setOptions('foo', '') as that still leaves an entry which is
// rendered rather than removing the entry from the query string altogether.
// Calling $url->setOption() still leaves a value behind. Instead we work
// with the entire options array and remove items from it as needed.
$urlOptions = $url->getOptions();
if ($element['#multiple']) {
$newQuery = isset($existing_query[$name]) ? $existing_query[$name] : [];
if (in_array($optionValue, $selectedValues)) {
// Allow users to toggle an option using the same link.
$newQuery = array_filter($newQuery, function ($value) use ($selectedValues) {
return !in_array($value, $selectedValues);
});
}
else {
$newQuery[] = $optionValue;
}
if (empty($newQuery)) {
unset($urlOptions['query'][$name]);
}
else {
$urlOptions['query'][$name] = $newQuery;
}
}
else {
if ($optionValue == $element['#value']) {
// Allow toggle link functionality -- click the same link to turn an
// option on or off.
$newQuery = $existing_query;
unset($newQuery[$name]);
if (empty($newQuery)) {
// Remove the query string completely.
unset($urlOptions['query']);
}
else {
$urlOptions['query'] = $newQuery;
}
}
else {
$urlOptions['query'] = $existing_query;
$urlOptions['query'][$name] = $optionValue;
}
}
// Add our updated options to the Url object.
$url->setOptions($urlOptions);
// Provide the Twig template with an array of links.
$variables['links'][$optionValue] = [
'#attributes' => [
'id' => Html::getUniqueId('edit-' . implode('-', [$name, $optionValue])),
'name' => $name . '[' . $optionValue . ']',
'class' => [
'bef-link',
],
],
'#type' => 'link',
'#title' => $optionLabel,
'#url' => $url,
];
if (in_array($optionValue, $selectedValues)) {
$variables['links'][$optionValue]['#attributes']['class'][] = 'bef-link--selected';
}
}
// Handle nested links. But first add the links as children to the element
// for consistent processing between checkboxes/radio buttons and links.
$variables['element'] = array_replace($variables['element'], $variables['links']);
$variables['children'] = Element::children($variables['element']);
if (!empty($variables['element']['#bef_nested'])) {
_bef_preprocess_nested_elements($variables);
}
}
/******************************************************************************
* Utility functions for BEF themed elements.
******************************************************************************/
/**
* Internal function to handled nested form elements.
*
* Adds 'is_nested' and 'depth' $variables. Requires 'children' to be set in
* variables array before being called.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the exposed form element.
*/
function _bef_preprocess_nested_elements(array &$variables) {
// Provide a hierarchical info on the element children for the template to
// render as a nested <ul>. Views prepends '-' characters for each level of
// depth in the vocabulary. Store that information, but remove the hyphens as
// we don't want to display them.
$variables['is_nested'] = TRUE;
$variables['depth'] = [];
foreach ($variables['children'] as $child) {
if ($child === 'All') {
// For non-required filters, put the any/all option at the root.
$variables['depth'][$child] = 0;
// And don't change the text as it defaults to "- Any -" and we do not
// want to remove the leading hyphens.
continue;
}
$original = $variables['element'][$child]['#title'];
$variables['element'][$child]['#title'] = ltrim($original, '-');
$variables['depth'][$child] = strlen($original) - strlen($variables['element'][$child]['#title']);
}
}
/**
* @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\better_exposed_filters\BetterExposedFiltersHelper;
use Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetBase;
use Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\views\Plugin\views\filter\NumericFilter;
use Drupal\views\Plugin\views\filter\StringFilter;
/**
* Base class for Better exposed filters widget plugins.
*/
abstract class FilterWidgetBase extends BetterExposedFiltersWidgetBase implements BetterExposedFiltersWidgetInterface {
use StringTranslationTrait;
/**
* {@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;
}
// Check various filter types and determine what options are available.
if (is_a($filter, 'Drupal\views\Plugin\views\filter\String') || is_a($filter, 'Drupal\views\Plugin\views\filter\InOperator')) {
if (in_array($filter->operator, ['in', 'or', 'and', 'not'])) {
$is_applicable = TRUE;
}
if (in_array($filter->operator, ['empty', 'not empty'])) {
$is_applicable = TRUE;
}
}
if (is_a($filter, 'Drupal\views\Plugin\views\filter\BooleanOperator')) {
$is_applicable = TRUE;
}
if (is_a($filter, 'Drupal\taxonomy\Plugin\views\filter\TaxonomyIndexTid')) {
// Autocomplete and dropdown taxonomy filter are both instances of
// TaxonomyIndexTid, but we can't show BEF options for the autocomplete
// widget.
if ($filter_options['type'] == 'select') {
$is_applicable = TRUE;
}
}
if ($filter->isAGroup()) {
$is_applicable = TRUE;
}
return $is_applicable;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'advanced' => [
'collapsible' => FALSE,
'is_secondary' => FALSE,
'placeholder_text' => '',
'rewrite' => [
'filter_rewrite_values' => '',
],
'sort_options' => FALSE,
],
];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$filter = $this->handler;
$filter_widget_type = $this->getExposedFilterWidgetType();
$form['advanced'] = [
'#type' => 'details',
'#title' => $this->t('Advanced filter options'),
'#weight' => 10,
];
// Allow users to sort options.
$supported_types = ['select'];
if (in_array($filter_widget_type, $supported_types)) {
$form['advanced']['sort_options'] = [
'#type' => 'checkbox',
'#title' => 'Sort filter options',
'#default_value' => !empty($this->configuration['advanced']['sort_options']),
'#description' => $this->t('The options will be sorted alphabetically.'),
];
}
// Allow users to specify placeholder text.
$supported_types = ['entity_autocomplete', 'textfield'];
if (in_array($filter_widget_type, $supported_types)) {
$form['advanced']['placeholder_text'] = [
'#type' => 'textfield',
'#title' => $this->t('Placeholder text'),
'#description' => $this->t('Text to be shown in the text field until it is edited. Leave blank for no placeholder to be set.'),
'#default_value' => $this->configuration['advanced']['placeholder_text'],
];
}
// Allow rewriting of filter options for any filter. String and numeric
// filters allow unlimited filter options via textfields, so we can't
// offer rewriting for those.
// @TODO: check other core filter types
if ((!$filter instanceof StringFilter && !$filter instanceof NumericFilter) || $filter->isAGroup()) {
$form['advanced']['rewrite']['filter_rewrite_values'] = [
'#type' => 'textarea',
'#title' => $this->t('Rewrite the text displayed'),
'#default_value' => $this->configuration['advanced']['rewrite']['filter_rewrite_values'],
'#description' => $this->t('Use this field to rewrite the filter options displayed. Use the format of current_text|replacement_text, one replacement per line. For example: <pre>
Current|Replacement
On|Yes
Off|No
</pre> Leave the replacement text blank to remove an option altogether. If using hierarchical taxonomy filters, do not including leading hyphens in the current text.
'),
];
}
// Allow any filter to be collapsible.
$form['advanced']['collapsible'] = [
'#type' => 'checkbox',
'#title' => $this->t('Make filter options collapsible'),
'#default_value' => !empty($this->configuration['advanced']['collapsible']),
'#description' => $this->t(
'Puts the filter options in a collapsible details element.'
),
];
// Allow any filter to be moved into the secondary options element.
$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) {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$filter = $this->handler;
$filter_id = $filter->options['expose']['identifier'];
$field_id = $this->getExposedFilterFieldId();
$is_collapsible = $this->configuration['advanced']['collapsible'];
$is_secondary = !empty($form['secondary']) && $this->configuration['advanced']['is_secondary'];
// Sort options alphabetically.
if ($this->configuration['advanced']['sort_options']) {
$form[$field_id]['#nested'] = $filter->options['hierarchy'] ?? FALSE;
$form[$field_id]['#nested_delimiter'] = '-';
$form[$field_id]['#pre_process'][] = [$this, 'processSortedOptions'];
}
// Check for placeholder text.
if (!empty($this->configuration['advanced']['placeholder_text'])) {
// @todo: Add token replacement for placeholder text.
$form[$field_id]['#placeholder'] = $this->configuration['advanced']['placeholder_text'];
}
// Handle filter value rewrites.
if ($this->configuration['advanced']['rewrite']['filter_rewrite_values']) {
$form[$field_id]['#options'] = BetterExposedFiltersHelper::rewriteOptions($form[$field_id]['#options'], $this->configuration['advanced']['rewrite']['filter_rewrite_values']);
// @todo what is $selected?
// if (isset($selected) && !isset($form[$field_id]['#options'][$selected])) {
// Avoid "Illegal choice" errors.
// $form[$field_id]['#default_value'] = NULL;
// }
}
// Identify all exposed filter elements.
$identifier = $filter_id;
$exposed_label = $filter->options['expose']['label'];
$exposed_description = $filter->options['expose']['description'];
if ($filter->isAGroup()) {
$identifier = $filter->options['group_info']['identifier'];
$exposed_label = $filter->options['group_info']['label'];
$exposed_description = $filter->options['group_info']['description'];
}
// If selected, collect our collapsible filter form element and put it in
// a details element.
if ($is_collapsible) {
$form[$field_id . '_collapsible'] = [
'#type' => 'details',
'#title' => $exposed_label,
];
if ($is_secondary) {
// Move secondary elements.
$form[$field_id . '_collapsible']['#group'] = 'secondary';
}
}
$filter_elements = [
$identifier,
$filter->options['expose']['operator_id'],
];
// Iterate over all exposed filter elements.
foreach ($filter_elements as $element) {
// Sanity check to make sure the element exists.
if (empty($form[$element])) {
continue;
}
// Move collapsible elements.
if ($is_collapsible) {
$this->addElementToGroup($form, $form_state, $element, $field_id . '_collapsible');
}
else {
$form[$element]['#title'] = $exposed_label;
$form[$element]['#description'] = $exposed_description;
// Move secondary elements.
if ($is_secondary) {
$this->addElementToGroup($form, $form_state, $element, 'secondary');
}
}
// Finally, add some metadata to the form element.
$this->addContext($form[$element]);
}
}
/**
* Sorts the options for a given form element alphabetically.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state.
*
* @return array
* The altered element.
*/
public function processSortedOptions(array $element, FormStateInterface $form_state) {
$options = &$element['#options'];
// Ensure "- Any -" value does not get sorted.
$any_option = FALSE;
if (empty($element['#required'])) {
// We use array_slice to preserve they keys needed to determine the value
// when using a filter (e.g. taxonomy terms).
$any_option = array_slice($options, 0, 1, TRUE);
// Array_slice does not modify the existing array, we need to remove the
// option manually.
unset($options[key($any_option)]);
}
// Not all option arrays will have simple data types. We perform a custom
// sort in case users want to sort more complex fields (e.g taxonomy terms).
if (!empty($element['#nested'])) {
$delimiter = $element['#nested_delimiter'] ?? '-';
$options = BetterExposedFiltersHelper::sortNestedOptions($options, $delimiter);
}
else {
$options = BetterExposedFiltersHelper::sortOptions($options);
}
// Restore the "- Any -" value at the first position.
if ($any_option) {
$options = $any_option + $options;
}
return $element;
}
/**
* Helper function to get the unique identifier for the exposed filter.
*
* Takes into account grouped filters with custom identifiers.
*/
protected function getExposedFilterFieldId() {
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$filter = $this->handler;
$field_id = $filter->options['expose']['identifier'];
$is_grouped_filter = $filter->options['is_grouped'] ?: FALSE;
// Grouped filters store their identifier elsewhere.
if ($is_grouped_filter) {
$field_id = $filter->options['group_info']['identifier'];
}
return $field_id;
}
/**
* Helper function to get the widget type of the exposed filter.
*
* @return string
* The type of the form render element use for the exposed filter.
*/
protected function getExposedFilterWidgetType() {
// We need to dig into the exposed form configuration to retrieve the
// form type of the filter.
$form = [];
$form_state = new FormState();
$form_state->set('exposed', TRUE);
/** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
$filter = $this->handler;
$filter_id = $filter->options['expose']['identifier'];
return $form[$filter_id]['#type'] ?? $form[$filter_id]['value']['#type'] ?? '';
}
}
<?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';
}
}
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\better_exposed_filters\sort;
use Drupal\better_exposed_filters\BetterExposedFiltersHelper;
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 SortWidgetBase extends BetterExposedFiltersWidgetBase implements BetterExposedFiltersWidgetInterface {
use StringTranslationTrait;
/**
* List of available exposed sort form element keys.
*
* @var array
*/
protected $sortElements = [
'sort_bef_combine',
'sort_by',
'sort_order',
];
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'advanced' => [
'collapsible' => FALSE,
'collapsible_label' => $this->t('Sort options'),
'combine' => FALSE,
'combine_rewrite' => '',
'is_secondary' => FALSE,
'reset' => FALSE,
'reset_label' => '',
],
];
}
/**
* {@inheritdoc}
*/
public static function isApplicable($handler = NULL, array $options = []) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = [];
$form['advanced'] = [
'#type' => 'details',
'#title' => $this->t('Advanced sort options'),
];
// We can only combine sort order and sort by if both options are exposed.
$form['advanced']['combine'] = [
'#type' => 'checkbox',
'#title' => $this->t('Combine sort order with sort by'),
'#default_value' => !empty($this->configuration['advanced']['combine']),
'#description' => $this->t('Combines the sort by options and order (ascending or decending) into a single list. Use this to display "Option1 (ascending)", "Option1 (descending)", "Option2 (ascending)", "Option2 (descending)" in a single form element. Sort order should first be exposed by selecting <em>Allow people to choose the sort order</em>.'),
'#states' => [
'enabled' => [
':input[name="exposed_form_options[expose_sort_order]"]' => ['checked' => TRUE],
],
],
];
$form['advanced']['combine_rewrite'] = [
'#type' => 'textarea',
'#title' => $this->t('Rewrite the text displayed'),
'#default_value' => $this->configuration['advanced']['combine_rewrite'],
'#description' => $this->t('Use this field to rewrite the text displayed for combined sort options and sort order. Use the format of current_text|replacement_text, one replacement per line. For example: <pre>
Post date Asc|Oldest first
Post date Desc|Newest first
Title Asc|A -> Z
Title Desc|Z -> A</pre> Leave the replacement text blank to remove an option altogether. The order the options appear will be changed to match the order of options in this field.'),
'#states' => [
'visible' => [
':input[name="exposed_form_options[bef][sort][configuration][advanced][combine]"]' => ['checked' => TRUE],
],
],
];
$form['advanced']['reset'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include a "Reset sort" option'),
'#default_value' => !empty($this->configuration['advanced']['reset']),
'#description' => $this->t('Adds a "Reset sort" link; Views will use the default sort order.'),
];
$form['advanced']['reset_label'] = [
'#type' => 'textfield',
'#title' => $this->t('"Reset sort" label'),
'#default_value' => $this->configuration['advanced']['reset_label'],
'#description' => $this->t('This cannot be left blank if the above option is checked'),
'#states' => [
'visible' => [
':input[name="exposed_form_options[bef][sort][configuration][advanced][reset]"]' => ['checked' => TRUE],
],
'required' => [
':input[name="exposed_form_options[bef][sort][configuration][advanced][reset]"]' => ['checked' => TRUE],
],
],
];
$form['advanced']['collapsible'] = [
'#type' => 'checkbox',
'#title' => $this->t('Make sort options collapsible'),
'#default_value' => !empty($this->configuration['advanced']['collapsible']),
'#description' => $this->t(
'Puts the sort options in a collapsible details element.'
),
];
$form['advanced']['collapsible_label'] = [
'#type' => 'textfield',
'#title' => $this->t('Collapsible details element title'),
'#default_value' => $this->configuration['advanced']['collapsible_label'],
'#description' => $this->t('This cannot be left blank or there will be no way to show/hide sort options.'),
'#states' => [
'visible' => [
':input[name="exposed_form_options[bef][sort][configuration][advanced][collapsible]"]' => ['checked' => TRUE],
],
'required' => [
':input[name="exposed_form_options[bef][sort][configuration][advanced][collapsible]"]' => ['checked' => TRUE],
],
],
];
$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_collapsible = $this->configuration['advanced']['collapsible']
&& !empty($this->configuration['advanced']['collapsible_label']);
$is_secondary = !empty($form['secondary']) && $this->configuration['advanced']['is_secondary'];
// Check for combined sort_by and sort_order.
if ($this->configuration['advanced']['combine'] && !empty($form['sort_order'])) {
$options = [];
$selected = '';
foreach ($form['sort_by']['#options'] as $by_key => $by_val) {
foreach ($form['sort_order']['#options'] as $order_key => $order_val) {
// Use a space to separate the two keys, we'll unpack them in our
// submit handler.
$options[$by_key . '_' . $order_key] = "$by_val $order_val";
if ($form['sort_order']['#default_value'] === $order_key && empty($selected)) {
// Respect default sort order set in Views. The default sort field
// will be the first one if there are multiple sort criteria.
$selected = $by_key . '_' . $order_key;
}
}
}
// Rewrite the option values if any were specified.
if (!empty($this->configuration['advanced']['combine_rewrite'])) {
$options = BetterExposedFiltersHelper::rewriteOptions($options, $this->configuration['advanced']['combine_rewrite'], TRUE);
if (!isset($options[$selected])) {
// Avoid "illegal choice" errors if the selected option is
// eliminated by the rewrite.
$selected = NULL;
}
}
// Add reset sort option at the top of the list.
if ($this->configuration['advanced']['reset']) {
$options = [' ' => $this->configuration['advanced']['reset_label']] + $options;
}
$form['sort_bef_combine'] = [
'#type' => 'select',
'#options' => $options,
'#default_value' => $selected,
// Already sanitized by Views.
'#title' => $form['sort_by']['#title'],
];
// Add our submit routine to process.
$form['#submit'][] = [$this, 'sortCombineSubmitForm'];
// Pretend we're another exposed form widget.
$form['#info']['sort-sort_bef_combine'] = [
'value' => 'sort_bef_combine',
];
// Remove the existing sort_by and sort_order elements.
unset($form['sort_by']);
unset($form['sort_order']);
}
else {
// Add reset sort option if selected.
if ($this->configuration['advanced']['reset']) {
array_unshift($form['sort_by']['#options'], $this->configuration['advanced']['reset_label']);
}
}
// If selected, collect all sort-related form elements and put them in a
// details element.
if ($is_collapsible) {
$form['bef_sort_options'] = [
'#type' => 'details',
'#title' => $this->configuration['advanced']['collapsible_label'],
];
if ($is_secondary) {
// Move secondary elements.
$form['bef_sort_options']['#group'] = 'secondary';
}
}
// Iterate over all exposed sort elements.
foreach ($this->sortElements as $element) {
// Sanity check to make sure the element exists.
if (empty($form[$element])) {
continue;
}
// Move collapsible elements.
if ($is_collapsible) {
$this->addElementToGroup($form, $form_state, $element, 'bef_sort_options');
}
// Move secondary elements.
elseif ($is_secondary) {
$this->addElementToGroup($form, $form_state, $element, 'secondary');
}
// Finally, add some metadata to the form element.
$this->addContext($form[$element]);
}
}
/**
* Unpacks sort_by and sort_order from the sort_bef_combine element.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function sortCombineSubmitForm(array $form, FormStateInterface $form_state) {
$sort_by = $sort_order = '';
$combined = $form_state->getValue('sort_bef_combine');
if (!empty($combined)) {
$parts = explode('_', $combined);
$sort_order = trim(array_pop($parts));
$sort_by = trim(implode('_', $parts));
}
$form_state->setValue('sort_by', $sort_by);
$form_state->setValue('sort_order', $sort_order);
}
}
<?php
namespace Drupal\better_exposed_filters\Plugin\views\exposed_form;
use Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\views\Plugin\views\exposed_form\InputRequired;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Exposed form plugin that provides a basic exposed form.
*
* @ingroup views_exposed_form_plugins
*
* @ViewsExposedForm(
* id = "bef",
* title = @Translation("Better Exposed Filters"),
* help = @Translation("Provides additional options for exposed form elements.")
* )
*/
class BetterExposedFilters extends InputRequired {
/**
* BEF filters widget plugin manager.
*
* @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager
*/
public $filterWidgetManager;
/**
* BEF pager widget plugin manager.
*
* @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager
*/
public $pagerWidgetManager;
/**
* BEF sort widget plugin manager.
*
* @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager
*/
public $sortWidgetManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* BetterExposedFilters constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager $filter_widget_manager
* The better exposed filter widget manager for filter widgets.
* @param \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager $pager_widget_manager
* The better exposed filter widget manager for pager widgets.
* @param \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetManager $sort_widget_manager
* The better exposed filter widget manager for sort widgets.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* Manage drupal modules.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, BetterExposedFiltersWidgetManager $filter_widget_manager, BetterExposedFiltersWidgetManager $pager_widget_manager, BetterExposedFiltersWidgetManager $sort_widget_manager, ModuleHandlerInterface $module_handler) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->filterWidgetManager = $filter_widget_manager;
$this->pagerWidgetManager = $pager_widget_manager;
$this->sortWidgetManager = $sort_widget_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.better_exposed_filters_filter_widget'),
$container->get('plugin.manager.better_exposed_filters_pager_widget'),
$container->get('plugin.manager.better_exposed_filters_sort_widget'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
// General, sort, pagers, and filter.
$bef_options = [
'general' => [
'autosubmit' => FALSE,
'autosubmit_exclude_textfield' => FALSE,
'autosubmit_textfield_delay' => 500,
'autosubmit_hide' => FALSE,
'input_required' => FALSE,
'allow_secondary' => FALSE,
'secondary_label' => $this->t('Advanced options'),
'secondary_open' => FALSE,
],
'sort' => [
'plugin_id' => 'default',
],
];
// Initialize options if any sort is exposed.
// Iterate over each sort and determine if any sorts are exposed.
$is_sort_exposed = FALSE;
/* @var \Drupal\views\Plugin\views\HandlerBase $sort */
foreach ($this->view->display_handler->getHandlers('sort') as $sort) {
if ($sort->isExposed()) {
$is_sort_exposed = TRUE;
break;
}
}
if ($is_sort_exposed) {
$bef_options['sort']['plugin_id'] = 'default';
}
// Initialize options if the pager is exposed.
$pager = $this->view->getPager();
if ($pager && $pager->usesExposed()) {
$bef_options['pager']['plugin_id'] = 'default';
}
// Go through each exposed filter and set default format.
/* @var \Drupal\views\Plugin\views\HandlerBase $filter */
foreach ($this->view->display_handler->getHandlers('filter') as $filter_id => $filter) {
if (!$filter->isExposed()) {
continue;
}
$bef_options['filter'][$filter_id]['plugin_id'] = 'default';
}
// Iterate over bef options and convert them to be compatible with views
// default options.
$options += $this->createOptionDefaults(['bef' => $bef_options]);
return $options;
}
/**
* Creates a list of view handler default options.
*
* Views handlers expect default options in a specific format.
*
* @param array $options
* An array of plugin defaults.
*
* @return array
* An array of plugin options.
*
* @see \Drupal\views\Plugin\views\PluginBase::setOptionDefaults
*/
protected function createOptionDefaults(array $options) {
$result = [];
foreach ($options as $key => $option) {
if (is_array($option)) {
$result[$key]['contains'] = $this->createOptionDefaults($option);
}
else {
$result[$key]['default'] = $option;
}
}
return $result;
}
/**
* Build the views options form and adds custom options for BEF.
*
* @inheritDoc
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
// Ensure that the form values are stored in their original location, and
// not dependent on their position in the form tree. We are moving around
// a few elements to make the UI more user friendly.
$original_form = [];
parent::buildOptionsForm($original_form, $form_state);
foreach (Element::children($original_form) as $element) {
$original_form[$element]['#parents'] = ['exposed_form_options', $element];
}
// Save shorthand for BEF options.
$bef_options = $this->options['bef'];
// User raw user input for AJAX callbacks.
$user_input = $form_state->getUserInput();
$bef_input = $user_input['exposed_form_options']['bef'] ?? NULL;
/*
* General BEF settings
*/
// Reorder some existing form elements.
$form['bef']['general']['submit_button'] = $original_form['submit_button'];
$form['bef']['general']['reset_button'] = $original_form['reset_button'];
$form['bef']['general']['reset_button_label'] = $original_form['reset_button_label'];
// Add the 'auto-submit' functionality.
$form['bef']['general']['autosubmit'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable auto-submit'),
'#description' => $this->t('Automatically submits the form when an element has changed.'),
'#default_value' => $bef_options['general']['autosubmit'],
];
$form['bef']['general']['autosubmit_exclude_textfield'] = [
'#type' => 'checkbox',
'#title' => $this->t('Exclude Textfield'),
'#description' => $this->t('Exclude textfields from auto-submit. User will have to press enter key, or click submit button.'),
'#default_value' => $bef_options['general']['autosubmit_exclude_textfield'],
'#states' => [
'visible' => [
':input[name="exposed_form_options[bef][general][autosubmit]"]' => ['checked' => TRUE],
],
],
];
$form['bef']['general']['autosubmit_textfield_delay'] = [
'#type' => 'number',
'#title' => $this->t('Delay for textfield autosubmit'),
'#description' => $this->t('Configure a delay in ms before triggering autosubmit on textfields.'),
'#default_value' => $bef_options['general']['autosubmit_textfield_delay'],
'#min' => 0,
'#states' => [
'visible' => [
':input[name="exposed_form_options[bef][general][autosubmit]"]' => ['checked' => TRUE],
':input[name="exposed_form_options[bef][general][autosubmit_exclude_textfield]"]' => ['checked' => FALSE],
],
],
];
$form['bef']['general']['autosubmit_hide'] = [
'#type' => 'checkbox',
'#title' => $this->t('Hide submit button'),
'#description' => $this->t('Hides submit button if auto-submit and javascript are enabled.'),
'#default_value' => $bef_options['general']['autosubmit_hide'],
'#states' => [
'visible' => [
':input[name="exposed_form_options[bef][general][autosubmit]"]' => ['checked' => TRUE],
],
],
];
// Insert a checkbox to make the input required optional just before the
// input required text field. Only show the text field if the input required
// option is selected.
$form['bef']['general']['input_required'] = [
'#type' => 'checkbox',
'#title' => $this->t('Input required'),
'#description' => $this->t('Only display results after the user has selected a filter option.'),
'#default_value' => $bef_options['general']['input_required'],
];
$original_form['text_input_required'] += [
'#states' => [
'visible' => [
'input[name="exposed_form_options[bef][general][input_required]"]' => ['checked' => TRUE],
],
],
];
$form['bef']['general']['text_input_required'] = $original_form['text_input_required'];
/*
* Allow exposed form items to be displayed as secondary options.
*/
$form['bef']['general']['allow_secondary'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable secondary exposed form options'),
'#default_value' => $bef_options['general']['allow_secondary'],
'#description' => $this->t('Allows you to specify some exposed form elements as being secondary options and places those elements in a collapsible "details" element. Use this option to place some exposed filters in an "Advanced Search" area of the form, for example.'),
];
$form['bef']['general']['secondary_label'] = [
'#type' => 'textfield',
'#default_value' => $bef_options['general']['secondary_label'],
'#title' => $this->t('Secondary options label'),
'#description' => $this->t(
'The name of the details element to hold secondary options. This cannot be left blank or there will be no way to show/hide these options.'
),
'#states' => [
'required' => [
':input[name="exposed_form_options[bef][general][allow_secondary]"]' => ['checked' => TRUE],
],
'visible' => [
':input[name="exposed_form_options[bef][general][allow_secondary]"]' => ['checked' => TRUE],
],
],
];
$form['bef']['general']['secondary_open'] = [
'#type' => 'checkbox',
'#default_value' => $bef_options['general']['secondary_open'],
'#title' => $this->t('Secondary option open by default'),
'#description' => $this->t('Indicates whether the details element should be open by default.'),
'#states' => [
'visible' => [
':input[name="exposed_form_options[bef][general][allow_secondary]"]' => ['checked' => TRUE],
],
],
];
/*
* Add options for exposed sorts.
*/
// Add intro explaining BEF sorts.
$documentation_uri = Url::fromUri('http://drupal.org/node/1701012')->toString();
$form['bef']['sort']['bef_intro'] = [
'#markup' => '<h3>' . $this->t('Exposed Sort Settings') . '</h3><p>' . $this->t('This section lets you select additional options for exposed sorts. Some options are only available in certain situations. If you do not see the options you expect, please see the <a href=":link">BEF settings documentation page</a> for more details.', [':link' => $documentation_uri]) . '</p>',
];
// Iterate over each sort and determine if any sorts are exposed.
$is_sort_exposed = FALSE;
/* @var \Drupal\views\Plugin\views\HandlerBase $sort */
foreach ($this->view->display_handler->getHandlers('sort') as $sort) {
if ($sort->isExposed()) {
$is_sort_exposed = TRUE;
break;
}
}
$form['bef']['sort']['empty'] = [
'#type' => 'item',
'#description' => $this->t('No sort elements have been exposed yet.'),
'#access' => !$is_sort_exposed,
];
if ($is_sort_exposed) {
$options = [];
foreach ($this->sortWidgetManager->getDefinitions() as $plugin_id => $definition) {
if ($definition['class']::isApplicable()) {
$options[$plugin_id] = $definition['label'];
}
}
$form['bef']['sort']['configuration'] = [
'#prefix' => "<div id='bef-sort-configuration'>",
'#suffix' => "</div>",
'#type' => 'container',
];
// Get selected plugin_id on AJAX callback directly from the form state.
$selected_plugin_id = $bef_input['sort']['configuration']['plugin_id'] ??
$bef_options['sort']['plugin_id'];
$form['bef']['sort']['configuration']['plugin_id'] = [
'#type' => 'select',
'#title' => $this->t('Display exposed sort options as'),
'#default_value' => $selected_plugin_id,
'#options' => $options,
'#description' => $this->t('Select a format for the exposed sort options.'),
'#ajax' => [
'event' => 'change',
'effect' => 'fade',
'progress' => 'throbber',
// Since views options forms are complex, they're built by
// Drupal in a different way. To bypass this problem we need to
// provide the full path to the Ajax callback.
'callback' => __CLASS__ . '::ajaxCallback',
'wrapper' => 'bef-sort-configuration',
],
];
// Move some existing form elements.
$form['bef']['sort']['configuration']['exposed_sorts_label'] = $original_form['exposed_sorts_label'];
$form['bef']['sort']['configuration']['expose_sort_order'] = $original_form['expose_sort_order'];
$form['bef']['sort']['configuration']['sort_asc_label'] = $original_form['sort_asc_label'];
$form['bef']['sort']['configuration']['sort_desc_label'] = $original_form['sort_desc_label'];
if ($selected_plugin_id) {
$plugin_configuration = $bef_options['sort'] ?? [];
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->sortWidgetManager->createInstance($selected_plugin_id, $plugin_configuration);
$plugin->setView($this->view);
$subform = &$form['bef']['sort']['configuration'];
$subform_state = SubformState::createForSubform($subform, $form, $form_state);
$subform += $plugin->buildConfigurationForm($subform, $subform_state);
}
}
/*
* Add options for exposed pager.
*/
$documentation_uri = Url::fromUri('http://drupal.org/node/1701012')->toString();
$form['bef']['pager']['bef_intro'] = [
'#markup' => '<h3>' . $this->t('Exposed Pager Settings') . '</h3><p>' . $this->t('This section lets you select additional options for exposed pagers. Some options are only available in certain situations. If you do not see the options you expect, please see the <a href=":link">BEF settings documentation page</a> for more details.', [':link' => $documentation_uri]) . '</p>',
];
$pager = $this->view->getPager();
$is_pager_exposed = $pager && $pager->usesExposed();
$form['bef']['pager']['empty'] = [
'#type' => 'item',
'#description' => $this->t('No pager elements have been exposed yet.'),
'#access' => !$is_pager_exposed,
];
if ($is_pager_exposed) {
$options = [];
foreach ($this->pagerWidgetManager->getDefinitions() as $plugin_id => $definition) {
if ($definition['class']::isApplicable()) {
$options[$plugin_id] = $definition['label'];
}
}
$form['bef']['pager']['configuration'] = [
'#prefix' => "<div id='bef-pager-configuration'>",
'#suffix' => "</div>",
'#type' => 'container',
];
// Get selected plugin_id on AJAX callback directly from the form state.
$selected_plugin_id = $bef_input['pager']['configuration']['plugin_id'] ??
$bef_options['pager']['plugin_id'];
$form['bef']['pager']['configuration']['plugin_id'] = [
'#type' => 'select',
'#title' => $this->t('Display exposed pager options as'),
'#default_value' => $selected_plugin_id,
'#options' => $options,
'#description' => $this->t('Select a format for the exposed pager options.'),
'#ajax' => [
'event' => 'change',
'effect' => 'fade',
'progress' => 'throbber',
// Since views options forms are complex, they're built by
// Drupal in a different way. To bypass this problem we need to
// provide the full path to the Ajax callback.
'callback' => __CLASS__ . '::ajaxCallback',
'wrapper' => 'bef-pager-configuration',
],
];
if ($selected_plugin_id) {
$plugin_configuration = $bef_options['pager'] ?? [];
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->pagerWidgetManager->createInstance($selected_plugin_id, $plugin_configuration);
$plugin->setView($this->view);
$subform = &$form['bef']['pager']['configuration'];
$subform_state = SubformState::createForSubform($subform, $form, $form_state);
$subform += $plugin->buildConfigurationForm($subform, $subform_state);
}
}
/*
* Add options for exposed filters.
*/
$documentation_uri = Url::fromUri('http://drupal.org/node/1701012')->toString();
$form['bef']['filter']['bef_intro'] = [
'#markup' => '<h3>' . $this->t('Exposed Filter Settings') . '</h3><p>' . $this->t('This section lets you select additional options for exposed filters. Some options are only available in certain situations. If you do not see the options you expect, please see the <a href=":link">BEF settings documentation page</a> for more details.', [':link' => $documentation_uri]) . '</p>',
];
// Iterate over each filter and add BEF filter options.
/* @var \Drupal\views\Plugin\views\HandlerBase $filter */
foreach ($this->view->display_handler->getHandlers('filter') as $filter_id => $filter) {
if (!$filter->isExposed()) {
continue;
}
$options = [];
foreach ($this->filterWidgetManager->getDefinitions() as $plugin_id => $definition) {
if ($definition['class']::isApplicable($filter, $this->displayHandler->handlers['filter'][$filter_id]->options)) {
$options[$plugin_id] = $definition['label'];
}
}
// Alter the list of available widgets for this filter.
$this->moduleHandler->alter('better_exposed_filters_display_options', $options, $filter);
// Get a descriptive label for the filter.
$label = $this->t('Exposed filter @filter', [
'@filter' => $filter->options['expose']['identifier'],
]);
if (!empty($filter->options['expose']['label'])) {
$label = $this->t('Exposed filter "@filter" with label "@label"', [
'@filter' => $filter->options['expose']['identifier'],
'@label' => $filter->options['expose']['label'],
]);
}
$form['bef']['filter'][$filter_id] = [
'#type' => 'details',
'#title' => $label,
'#collapsed' => FALSE,
'#collapsible' => TRUE,
];
$form['bef']['filter'][$filter_id]['configuration'] = [
'#prefix' => "<div id='bef-filter-$filter_id-configuration'>",
'#suffix' => "</div>",
'#type' => 'container',
];
// Get selected plugin_id on AJAX callback directly from the form state.
$selected_plugin_id = $bef_input['filter'][$filter_id]['configuration']['plugin_id'] ?? $bef_options['filter'][$filter_id]['plugin_id'];
$form['bef']['filter'][$filter_id]['configuration']['plugin_id'] = [
'#type' => 'select',
'#title' => $this->t('Exposed filter widget:'),
'#default_value' => $selected_plugin_id,
'#options' => $options,
'#ajax' => [
'event' => 'change',
'effect' => 'fade',
'progress' => 'throbber',
// Since views options forms are complex, they're built by
// Drupal in a different way. To bypass this problem we need to
// provide the full path to the Ajax callback.
'callback' => __CLASS__ . '::ajaxCallback',
'wrapper' => 'bef-filter-' . $filter_id . '-configuration',
],
];
if ($selected_plugin_id) {
$plugin_configuration = $bef_options['filter'][$filter_id] ?? [];
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->filterWidgetManager->createInstance($selected_plugin_id, $plugin_configuration);
$plugin->setView($this->view);
$plugin->setViewsHandler($filter);
$subform = &$form['bef']['filter'][$filter_id]['configuration'];
$subform_state = SubformState::createForSubform($subform, $form, $form_state);
$subform += $plugin->buildConfigurationForm($subform, $subform_state);
}
}
}
/**
* The form ajax callback.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* The form element to return.
*/
public static function ajaxCallback(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
return NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -1));
}
/**
* {@inheritdoc}
*/
public function validateOptionsForm(&$form, FormStateInterface $form_state) {
// Drupal only passes in a part of the views form, but we need the complete
// form array for plugin subforms to work.
$parent_form = $form_state->getCompleteForm();
// Save a shorthand to the BEF form.
$bef_form = &$form['bef'];
// Save a shorthand to the BEF options.
$bef_form_options = $form_state->getValue(['exposed_form_options', 'bef']);
parent::validateOptionsForm($form, $form_state);
// Skip plugin validation if we are switching between bef plugins.
$triggering_element = $form_state->getTriggeringElement();
if ($triggering_element['#type'] !== 'submit') {
return;
}
// Shorthand for all filter handlers in this view.
/* @var \Drupal\views\Plugin\views\HandlerBase[] $filters */
$filters = $this->view->display_handler->handlers['filter'];
// Iterate over all filter, sort and pager plugins.
foreach ($bef_form_options as $type => $config) {
// Validate exposed filter configuration.
if ($type === 'filter') {
foreach ($config as $filter_id => $filter_options) {
$plugin_id = $filter_options['configuration']['plugin_id'] ?? NULL;
if (!$plugin_id) {
continue;
}
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->filterWidgetManager->createInstance($plugin_id);
$subform = &$bef_form[$type][$filter_id]['configuration'];
$subform_state = SubformState::createForSubform($subform, $parent_form, $form_state);
$plugin->setView($this->view);
$plugin->setViewsHandler($filters[$filter_id]);
$plugin->validateConfigurationForm($subform, $subform_state);
}
}
// Validate exposed pager/sort configuration.
elseif (in_array($type, ['pager', 'sort'])) {
$plugin_id = $config['configuration']['plugin_id'] ?? NULL;
if (!$plugin_id) {
continue;
}
// Use the correct widget manager.
if ($type === 'pager') {
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->pagerWidgetManager->createInstance($plugin_id);
}
else {
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->sortWidgetManager->createInstance($plugin_id);
}
$subform = &$bef_form[$type]['configuration'];
$subform_state = SubformState::createForSubform($subform, $parent_form, $form_state);
$plugin->setView($this->view);
$plugin->validateConfigurationForm($subform, $subform_state);
}
}
}
/**
* {@inheritdoc}
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
// Drupal only passes in a part of the views form, but we need the complete
// form array for plugin subforms to work.
$parent_form = $form_state->getCompleteForm();
// Save a shorthand to the BEF form.
$bef_form = &$form['bef'];
// Reorder options based on config - some keys may have shifted because of
// form alterations (@see \Drupal\better_exposed_filters\Plugin\views\exposed_form\BetterExposedFilters::buildOptionsForm).
$options = &$form_state->getValue('exposed_form_options');
$options = array_replace_recursive($this->options, $options);
// Save a shorthand to the BEF options.
$bef_options = &$options['bef'];
// Shorthand for all filter handlers in this view.
/* @var \Drupal\views\Plugin\views\HandlerBase[] $filters */
$filters = $this->view->display_handler->handlers['filter'];
parent::submitOptionsForm($form, $form_state);
// Iterate over all filter, sort and pager plugins.
foreach ($bef_options as $type => $config) {
// Save exposed filter configuration.
if ($type === 'filter') {
foreach ($config as $filter_id => $filter_options) {
$plugin_id = $filter_options['configuration']['plugin_id'] ?? NULL;
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
if (!$plugin_id) {
unset($bef_options['filter'][$filter_id]);
continue;
}
$plugin = $this->filterWidgetManager->createInstance($plugin_id);
$subform = &$bef_form[$type][$filter_id]['configuration'];
$subform_state = SubformState::createForSubform($subform, $parent_form, $form_state);
$plugin->setView($this->view);
$plugin->setViewsHandler($filters[$filter_id]);
$plugin->submitConfigurationForm($subform, $subform_state);
$plugin_configuration = $plugin->getConfiguration();
$bef_options[$type][$filter_id] = $plugin_configuration;
}
}
// Save exposed pager/sort configuration.
elseif (in_array($type, ['pager', 'sort'])) {
$plugin_id = $config['configuration']['plugin_id'] ?? NULL;
if (!$plugin_id) {
unset($bef_options[$type]);
continue;
}
// Use the correct widget manager.
if ($type === 'pager') {
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->pagerWidgetManager->createInstance($plugin_id);
}
else {
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->sortWidgetManager->createInstance($plugin_id);
}
$subform = &$bef_form[$type]['configuration'];
$subform_state = SubformState::createForSubform($subform, $parent_form, $form_state);
$plugin->setView($this->view);
$plugin->submitConfigurationForm($subform, $subform_state);
$plugin_configuration = $plugin->getConfiguration();
$bef_options[$type] = $plugin_configuration;
}
}
}
/**
* {@inheritdoc}
*/
public function exposedFormAlter(&$form, FormStateInterface $form_state) {
parent::exposedFormAlter($form, $form_state);
// Mark form as Better Exposed Filter form for easier alterations.
$form['#context']['bef'] = TRUE;
// These styles are used on all exposed forms.
$form['#attached']['library'][] = 'better_exposed_filters/general';
// Add the bef-exposed-form class at the form level so we can limit some
// styling changes to just BEF forms.
$form['#attributes']['class'][] = 'bef-exposed-form';
// Grab BEF options and allow modules/theme to modify them before
// processing.
$bef_options = $this->options['bef'];
$this->moduleHandler->alter('better_exposed_filters_options', $bef_options, $this->view, $this->displayHandler);
// Apply auto-submit values.
if (!empty($bef_options['general']['autosubmit'])) {
$form = array_merge_recursive($form, [
'#attributes' => [
'data-bef-auto-submit-full-form' => '',
'data-bef-auto-submit' => '',
'data-bef-auto-submit-delay' => $bef_options['general']['autosubmit_textfield_delay'],
],
]);
$form['actions']['submit']['#attributes']['data-bef-auto-submit-click'] = '';
$form['#attached']['library'][] = 'better_exposed_filters/auto_submit';
if (!empty($bef_options['general']['autosubmit_exclude_textfield'])) {
$supported_types = ['entity_autocomplete', 'textfield'];
foreach ($form as &$element) {
$element_type = $element['#type'] ?? NULL;
if (in_array($element_type, $supported_types)) {
$element['#attributes']['data-bef-auto-submit-exclude'] = '';
}
}
}
if (!empty($bef_options['general']['autosubmit_hide'])) {
$form['actions']['submit']['#attributes']['class'][] = 'js-hide';
}
}
// Some elements may be placed in a secondary details element (eg: "Advanced
// search options"). Place this after the exposed filters and before the
// rest of the items in the exposed form.
$allow_secondary = $bef_options['general']['allow_secondary'];
if ($allow_secondary) {
$form['secondary'] = [
'#attributes' => [
'class' => ['bef--secondary'],
],
'#type' => 'details',
'#title' => $bef_options['general']['secondary_label'],
'#open' => $bef_options['general']['secondary_open'],
// Disable until fields are added to this fieldset.
'#access' => FALSE,
];
}
/*
* Handle exposed sort elements.
*/
if (isset($bef_options['sort']['plugin_id']) && !empty($form['sort_by'])) {
$plugin_id = $bef_options['sort']['plugin_id'];
$plugin_configuration = $bef_options['sort'];
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->sortWidgetManager->createInstance($plugin_id, $plugin_configuration);
$plugin->setView($this->view);
$plugin->exposedFormAlter($form, $form_state);
}
/*
* Handle exposed pager elements.
*/
$pager = $this->view->getPager();
$is_pager_exposed = $pager && $pager->usesExposed();
if ($is_pager_exposed && !empty($bef_options['pager']['plugin_id'])) {
$plugin_id = $bef_options['pager']['plugin_id'];
$plugin_configuration = $bef_options['pager'];
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->pagerWidgetManager->createInstance($plugin_id, $plugin_configuration);
$plugin->setView($this->view);
$plugin->exposedFormAlter($form, $form_state);
}
/*
* Handle exposed filters.
*/
// Shorthand for all filter handlers in this view.
/* @var \Drupal\views\Plugin\views\HandlerBase[] $filters */
$filters = $this->view->display_handler->handlers['filter'];
// Iterate over all exposed filters.
if (!empty($bef_options['filter'])) {
foreach ($bef_options['filter'] as $filter_id => $filter_options) {
// Sanity check: Ensure this filter is an exposed filter.
if (empty($filters[$filter_id]) || !$filters[$filter_id]->isExposed()) {
continue;
}
$plugin_id = $filter_options['plugin_id'];
if ($plugin_id) {
/** @var \Drupal\better_exposed_filters\Plugin\BetterExposedFiltersWidgetInterface $plugin */
$plugin = $this->filterWidgetManager->createInstance($plugin_id, $filter_options);
$plugin->setView($this->view);
$plugin->setViewsHandler($filters[$filter_id]);
$plugin->exposedFormAlter($form, $form_state);
}
}
}
// If our form has no visible filters, hide the submit button.
$has_visible_filters = !empty(Element::getVisibleChildren($form)) ?: FALSE;
$form['actions']['submit']['#access'] = $has_visible_filters;
$form['actions']['reset']['#access'] = $has_visible_filters;
// Ensure default process/pre_render callbacks are included when a BEF
// widget has added their own.
foreach (Element::children($form) as $key) {
$element = &$form[$key];
$this->addDefaultElementInfo($element);
}
}
/**
* {@inheritdoc}
*/
protected function exposedFilterApplied() {
// If the input required option is set, check to see if a filter option has
// been set.
if (!empty($this->options['bef']['general']['input_required'])) {
return parent::exposedFilterApplied();
}
else {
return TRUE;
}
}
/**
* Inserts a new form element before another element identified by $key.
*
* This can be useful when reordering existing form elements without weights.
*
* @param array $form
* The form array to insert the element into.
* @param string $key
* The key of the form element you want to prepend the new form element.
* @param array $element
* The form element to insert.
*
* @return array
* The form array containing the newly inserted element.
*/
protected function prependFormElement(array $form, $key, array $element) {
$pos = array_search($key, array_keys($form)) + 1;
return array_splice($form, 0, $pos - 1) + $element + $form;
}
/**
* Adds default element callbacks.
*
* This is a workaround where adding process and pre-render functions are not
* results in replacing the default ones instead of merging.
*
* @param array $element
* The render array for a single form element.
*
* @todo remove once the following issues are resolved.
* @see https://www.drupal.org/project/drupal/issues/2070131
* @see https://www.drupal.org/project/drupal/issues/2190333
*/
protected function addDefaultElementInfo(array &$element) {
/** @var \Drupal\Core\Render\ElementInfoManager $element_info_manager */
$element_info = \Drupal::service('element_info');
if (isset($element['#type']) && empty($element['#defaults_loaded']) && ($info = $element_info->getInfo($element['#type']))) {
$element['#process'] = $element['#process'] ?? [];
$element['#pre_render'] = $element['#pre_render'] ?? [];
if (!empty($info['#process'])) {
$element['#process'] = array_merge($info['#process'], $element['#process']);
}
if (!empty($info['#pre_render'])) {
$element['#pre_render'] = array_merge($info['#pre_render'], $element['#pre_render']);
}
// Some processing needs to happen prior to the default form element
// callbacks (e.g. sort). We use the custom '#pre_process' array for this.
if (!empty($element['#pre_process'])) {
$element['#process'] = array_merge($element['#pre_process'], $element['#process']);
}
// Workaround to add support for #group FAPI to all elements currently not
// supported.
// @todo remove once core issue is resolved.
// @see https://www.drupal.org/project/drupal/issues/2190333
if (!in_array('processGroup', array_column($element['#process'], 1))) {
$element['#process'][] = ['\Drupal\Core\Render\Element\RenderElement', 'processGroup'];
$element['#pre_render'][] = ['\Drupal\Core\Render\Element\RenderElement', 'preRenderGroup'];
}
}
// Apply the same to any nested children.
foreach (Element::children($element) as $key) {
$child = &$element[$key];
$this->addDefaultElementInfo($child);
}
}
}
{#
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
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_bef_boolean
- field.storage.node.field_bef_email
- field.storage.node.field_bef_integer
- field.storage.node.field_bef_letters
- field.storage.node.field_bef_location
- field.storage.node.field_bef_price
- taxonomy.vocabulary.bef_test_location
enforced:
module:
- bef_test
module:
- better_exposed_filters
- node
- options
- taxonomy
- user
id: bef_test
label: 'BEF Test'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: bef
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
bef:
general:
autosubmit: false
autosubmit_exclude_textfield: false
autosubmit_hide: false
input_required: false
allow_secondary: false
secondary_label: 'Advanced options'
sort:
plugin_id: default
advanced:
combine: false
combine_rewrite: ''
reset: false
reset_label: ''
collapsible: false
collapsible_label: ''
is_secondary: false
pager:
plugin_id: default
advanced:
is_secondary: false
filter:
status:
plugin_id: default
advanced:
collapsible: false
is_secondary: false
placeholder_text: ''
rewrite:
filter_rewrite_values: ''
sort_options: false
field_bef_boolean_value:
plugin_id: default
advanced:
collapsible: false
is_secondary: false
placeholder_text: ''
rewrite:
filter_rewrite_values: ''
sort_options: false
field_bef_email_value:
plugin_id: default
advanced:
collapsible: false
is_secondary: false
placeholder_text: ''
rewrite:
filter_rewrite_values: ''
sort_options: false
field_bef_integer_value:
plugin_id: default
advanced:
collapsible: false
is_secondary: false
placeholder_text: ''
rewrite:
filter_rewrite_values: ''
sort_options: false
field_bef_letters_value:
plugin_id: default
advanced:
collapsible: false
is_secondary: false
placeholder_text: ''
rewrite:
filter_rewrite_values: ''
sort_options: false
field_bef_location_target_id:
plugin_id: default
advanced:
collapsible: false
is_secondary: false
placeholder_text: ''
rewrite:
filter_rewrite_values: ''
sort_options: false
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: true
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: true
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next ›'
first: '« First'
last: 'Last »'
quantity: 9
style:
type: table
row:
type: fields
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: Title
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
status:
id: status
table: node_field_data
field: status
relationship: none
group_type: group
admin_label: ''
label: 'Publishing status'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: boolean
settings:
format: default
format_custom_true: ''
format_custom_false: ''
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: status
plugin_id: field
field_bef_boolean:
id: field_bef_boolean
table: node__field_bef_boolean
field: field_bef_boolean
relationship: none
group_type: group
admin_label: ''
label: bef_boolean
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: boolean
settings:
format: default
format_custom_true: ''
format_custom_false: ''
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
plugin_id: field
field_bef_email:
id: field_bef_email
table: node__field_bef_email
field: field_bef_email
relationship: none
group_type: group
admin_label: ''
label: bef_email
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: basic_string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
plugin_id: field
field_bef_integer:
id: field_bef_integer
table: node__field_bef_integer
field: field_bef_integer
relationship: none
group_type: group
admin_label: ''
label: bef_integer
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: list_default
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
plugin_id: field
field_bef_letters:
id: field_bef_letters
table: node__field_bef_letters
field: field_bef_letters
relationship: none
group_type: group
admin_label: ''
label: bef_letters
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: list_default
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
plugin_id: field
field_bef_location:
id: field_bef_location
table: node__field_bef_location
field: field_bef_location
relationship: none
group_type: group
admin_label: ''
label: bef_location
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: true
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
plugin_id: field
field_bef_price:
id: field_bef_price
table: node__field_bef_price
field: field_bef_price
relationship: none
group_type: group
admin_label: ''
label: Price
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_decimal
settings:
thousand_separator: ''
prefix_suffix: true
decimal_separator: .
scale: 2
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
plugin_id: field
filters:
status:
id: status
table: node_field_data
field: status
relationship: none
group_type: group
admin_label: ''
operator: '='
value: '1'
group: 1
exposed: true
expose:
operator_id: ''
label: 'Published status'
description: ''
use_operator: false
operator: status_op
identifier: status
required: true
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: boolean
entity_type: node
entity_field: status
field_bef_email_value:
id: field_bef_email_value
table: node__field_bef_email
field: field_bef_email_value
relationship: none
group_type: group
admin_label: ''
operator: '='
value: ''
group: 1
exposed: true
expose:
operator_id: field_bef_email_value_op
label: 'bef_email (field_bef_email)'
description: ''
use_operator: false
operator: field_bef_email_value_op
identifier: field_bef_email_value
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
field_bef_integer_value:
id: field_bef_integer_value
table: node__field_bef_integer
field: field_bef_integer_value
relationship: none
group_type: group
admin_label: ''
operator: or
value: { }
group: 1
exposed: true
expose:
operator_id: field_bef_integer_value_op
label: 'bef_integer (field_bef_integer)'
description: ''
use_operator: false
operator: field_bef_integer_value_op
identifier: field_bef_integer_value
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
reduce_duplicates: false
plugin_id: list_field
field_bef_letters_value:
id: field_bef_letters_value
table: node__field_bef_letters
field: field_bef_letters_value
relationship: none
group_type: group
admin_label: ''
operator: or
value: { }
group: 1
exposed: true
expose:
operator_id: field_bef_letters_value_op
label: 'bef_letters (field_bef_letters)'
description: ''
use_operator: false
operator: field_bef_letters_value_op
identifier: field_bef_letters_value
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
reduce_duplicates: false
plugin_id: list_field
term_node_tid_depth:
id: term_node_tid_depth
table: node_field_data
field: term_node_tid_depth
relationship: none
group_type: group
admin_label: ''
operator: or
value: { }
group: 1
exposed: true
expose:
operator_id: term_node_tid_depth_op
label: 'Has taxonomy terms (with depth)'
description: ''
use_operator: false
operator: term_node_tid_depth_op
identifier: term_node_tid_depth
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
reduce_duplicates: false
type: select
limit: true
vid: bef_test_location
hierarchy: true
error_message: true
depth: 10
entity_type: node
plugin_id: taxonomy_index_tid_depth
field_bef_price_value:
id: field_bef_price_value
table: node__field_bef_price
field: field_bef_price_value
relationship: none
group_type: group
admin_label: ''
operator: '<='
value:
min: ''
max: ''
value: ''
group: 1
exposed: true
expose:
operator_id: field_bef_price_value_op
label: 'Price less than'
description: ''
use_operator: false
operator: field_bef_price_value_op
identifier: field_bef_price_value
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: numeric
field_bef_boolean_value:
id: field_bef_boolean_value
table: node__field_bef_boolean
field: field_bef_boolean_value
relationship: none
group_type: group
admin_label: ''
operator: '='
value: All
group: 1
exposed: true
expose:
operator_id: ''
label: 'bef_boolean (field_bef_boolean)'
description: ''
use_operator: false
operator: field_bef_boolean_value_op
identifier: field_bef_boolean_value
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
marketing_editor: '0'
vendor: '0'
partner: '0'
partner_admin: '0'
editor: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: boolean
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: true
expose:
label: 'Created'
granularity: second
title: 'BEF Test'
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: bef-test
page_2:
display_plugin: page
id: page_2
display_title: Page
position: 2
display_options:
display_extenders: { }
path: bef-test-with-block
exposed_block: true
<?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();
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel\Plugin\filter;
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 RadioButtonsFilterWidgetKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests the exposed checkboxes filter widget.
*/
public function testExposedCheckboxes() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Ensure our filter "field_bef_integer" allows multiple values.
$display['display_options']['filters']['field_bef_integer_value']['expose']['multiple'] = TRUE;
// Ensure our filter "term_node_tid_depth" has show hierarchy enabled.
$display['display_options']['filters']['term_node_tid_depth']['expose']['multiple'] = TRUE;
$display['display_options']['filters']['term_node_tid_depth']['hierarchy'] = TRUE;
// Change exposed filter "field_bef_integer" and "term_node_tid_depth" to
// checkboxes (i.e. 'bef').
$this->setBetterExposedOptions($view, [
'filter' => [
'field_bef_integer_value' => [
'plugin_id' => 'bef',
],
'term_node_tid_depth' => [
'plugin_id' => 'bef',
],
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our "FIELD_BEF_INTEGER" filter is rendered as checkboxes.
$actual = $this->xpath('//form//input[@type="checkbox" and starts-with(@name, "field_bef_integer_value")]');
$this->assertCount(5, $actual, 'Exposed filter "FIELD_BEF_INTEGER" has correct number of exposed checkboxes.');
// Check our "TERM_NODE_TID_DEPTH" filter is rendered as nested checkboxes.
$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"]/div/ul/li/div/input[@type="checkbox" and starts-with(@name, "term_node_tid_depth")]');
$this->assertCount(3, $actual, 'Exposed filter "TERM_NODE_TID_DEPTH" has correct number of exposed top-level checkboxes.');
$actual = $this->xpath('//form//div[@id="edit-term-node-tid-depth--2"]/div/ul/li/ul/li/div/input[@type="checkbox" and starts-with(@name, "term_node_tid_depth")]');
$this->assertCount(5, $actual, 'Exposed filter "TERM_NODE_TID_DEPTH" has correct number of exposed second-level checkboxes.');
$actual = $this->xpath('//form//div[@id="edit-term-node-tid-depth--2"]/div/ul/li/ul/li/ul/li/div/input[@type="checkbox" and starts-with(@name, "term_node_tid_depth")]');
$this->assertCount(14, $actual, 'Exposed filter "TERM_NODE_TID_DEPTH" has correct number of exposed third-level checkboxes.');
$view->destroy();
}
/**
* Tests the exposed radio buttons filter widget.
*/
public function testExposedRadioButtons() {
$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
// radio buttons (i.e. 'bef').
$this->setBetterExposedOptions($view, [
'filter' => [
'field_bef_boolean_value' => [
'plugin_id' => 'bef',
],
'term_node_tid_depth' => [
'plugin_id' => 'bef',
],
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our filter is rendered as radio buttons (i.e. Any, true, false).
$actual = $this->xpath('//form//input[@type="radio" and @name="field_bef_boolean_value"]');
$this->assertCount(3, $actual, 'Exposed filter "FIELD_BEF_BOOLEAN" renders as radio buttons.');
// Check our "TERM_NODE_TID_DEPTH" filter is rendered as nested radio
// buttons.
$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');
// The difference with checkboxes is that radio buttons render an additoinal
// top level option (i.e. any).
$actual = $this->xpath('//form//div[@id="edit-term-node-tid-depth--2"]/div/ul/li/div/input[@type="radio" and starts-with(@name, "term_node_tid_depth")]');
$this->assertCount(4, $actual, 'Exposed filter "TERM_NODE_TID_DEPTH" has correct number of exposed top-level radio buttons.');
$actual = $this->xpath('//form//div[@id="edit-term-node-tid-depth--2"]/div/ul/li/ul/li/div/input[@type="radio" and starts-with(@name, "term_node_tid_depth")]');
$this->assertCount(5, $actual, 'Exposed filter "TERM_NODE_TID_DEPTH" has correct number of exposed second-level radio buttons.');
$actual = $this->xpath('//form//div[@id="edit-term-node-tid-depth--2"]/div/ul/li/ul/li/ul/li/div/input[@type="radio" and starts-with(@name, "term_node_tid_depth")]');
$this->assertCount(14, $actual, 'Exposed filter "TERM_NODE_TID_DEPTH" has correct number of exposed third-level radio buttons.');
$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 single on/off filter widget.
*
* @group better_exposed_filters
*
* @see \Drupal\better_exposed_filters\Plugin\better_exposed_filters\filter\FilterWidgetBase
*/
class SingleFilterWidgetKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests hiding element with single option.
*/
public function testSingleExposedCheckbox() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Change exposed filter "field_bef_boolean" to single on/off (i.e.
// 'bef_single').
$this->setBetterExposedOptions($view, [
'filter' => [
'field_bef_boolean_value' => [
'plugin_id' => 'bef_single',
],
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our "FIELD_BEF_BOOLEAN" filter is rendered as a single checkbox.
$actual = $this->xpath('//form//input[@type="checkbox" and starts-with(@name, "field_bef_boolean_value")]');
$this->assertCount(1, $actual, 'Exposed filter "FIELD_BEF_BOOLEAN" is rendered as a checkbox.');
$view->destroy();
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel\Plugin\pager;
use Drupal\Tests\better_exposed_filters\Kernel\BetterExposedFiltersKernelTestBase;
use Drupal\views\Views;
/**
* Tests the links pager widget (i.e. "bef").
*
* @group better_exposed_filters
*
* @see \Drupal\better_exposed_filters\Plugin\better_exposed_filters\pager\RadioButtons
*/
class LinksPagerWidgetKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests the exposed links pager widget.
*/
public function testExposedLinks() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Ensure our pager exposes all items (i.e. items_per_page and offset).
$display['display_options']['pager']['options']['expose']['items_per_page'] = TRUE;
$display['display_options']['pager']['options']['expose']['offset'] = TRUE;
// Change exposed pager to radio buttons (i.e. 'bef').
$this->setBetterExposedOptions($view, [
'pager' => [
'plugin_id' => 'bef_links',
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our pager item "items_per_page" is rendered as links.
$actual = $this->xpath('//form//a[starts-with(@name, "items_per_page")]');
$this->assertCount(4, $actual, 'Exposed pager "items_per_page" has correct number of exposed radio buttons.');
$view->destroy();
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel\Plugin\pager;
use Drupal\Tests\better_exposed_filters\Kernel\BetterExposedFiltersKernelTestBase;
use Drupal\views\Views;
/**
* Tests the radio buttons pager widget (i.e. "bef").
*
* @group better_exposed_filters
*
* @see \Drupal\better_exposed_filters\Plugin\better_exposed_filters\pager\RadioButtons
*/
class RadioButtonsPagerWidgetKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests the exposed radio buttons pager widget.
*/
public function testExposedRadioButtons() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Ensure our pager exposes all items (i.e. items_per_page and offset).
$display['display_options']['pager']['options']['expose']['items_per_page'] = TRUE;
$display['display_options']['pager']['options']['expose']['offset'] = TRUE;
// Change exposed pager to radio buttons (i.e. 'bef').
$this->setBetterExposedOptions($view, [
'pager' => [
'plugin_id' => 'bef',
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our pager item "items_per_page" is rendered as radio buttons.
$actual = $this->xpath('//form//input[@type="radio" and starts-with(@name, "items_per_page")]');
$this->assertCount(4, $actual, 'Exposed pager "items_per_page" has correct number of exposed radio buttons.');
$view->destroy();
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel\Plugin\sort;
use Drupal\Tests\better_exposed_filters\Kernel\BetterExposedFiltersKernelTestBase;
use Drupal\views\Views;
/**
* Tests the links sort widget (i.e. "bef_links").
*
* @group better_exposed_filters
*
* @see \Drupal\better_exposed_filters\Plugin\better_exposed_filters\sort\Links
*/
class LinksSortWidgetKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests the exposed links sort widget.
*/
public function testExposedLinks() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Change exposed sort to links (i.e. 'bef_links').
$this->setBetterExposedOptions($view, [
'sort' => [
'plugin_id' => 'bef_links',
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our sort item "sort_by" is rendered as links.
$actual = $this->xpath('//form//a[starts-with(@id, "edit-sort-by")]');
$this->assertCount(1, $actual, 'Exposed sort "sort_by" has correct number of exposed links.');
// Check our sort item "sort_order" is rendered as links.
$actual = $this->xpath('//form//a[starts-with(@id, "edit-sort-order")]');
$this->assertCount(2, $actual, 'Exposed sort "sort_order" has correct number of exposed links.');
$view->destroy();
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel\Plugin\sort;
use Drupal\Tests\better_exposed_filters\Kernel\BetterExposedFiltersKernelTestBase;
use Drupal\views\Views;
/**
* Tests the radio buttons sort widget (i.e. "bef").
*
* @group better_exposed_filters
*
* @see \Drupal\better_exposed_filters\Plugin\better_exposed_filters\sort\RadioButtons
*/
class RadioButtonsSortWidgetKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests the exposed radio buttons sort widget.
*/
public function testExposedRadioButtons() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Change exposed sort to radio buttons (i.e. 'bef').
$this->setBetterExposedOptions($view, [
'sort' => [
'plugin_id' => 'bef',
],
]);
// Render the exposed form.
$this->renderExposedForm($view);
// Check our sort item "sort_by" is rendered as links.
$actual = $this->xpath('//form//input[@type="radio" and starts-with(@id, "edit-sort-by")]');
$this->assertCount(1, $actual, 'Exposed sort "sort_by" has correct number of exposed radio buttons.');
// Check our sort item "sort_order" is rendered as links.
$actual = $this->xpath('//form//input[@type="radio" and starts-with(@id, "edit-sort-order")]');
$this->assertCount(2, $actual, 'Exposed sort "sort_order" has correct number of exposed radio buttons.');
$view->destroy();
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Kernel\Plugin\sort;
use Drupal\Tests\better_exposed_filters\Kernel\BetterExposedFiltersKernelTestBase;
use Drupal\views\Views;
/**
* Tests the advanced options of a sort widget.
*
* @group better_exposed_filters
*
* @see \Drupal\better_exposed_filters\Plugin\better_exposed_filters\sort\SortWidgetBase
*/
class SortWidgetKernelTest extends BetterExposedFiltersKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['bef_test'];
/**
* Tests combining sort options (sort order and sort by).
*/
public function testCombineSortOptions() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Ensure our sort "created" is exposed.
$display['display_options']['sorts']['created']['exposed'] = TRUE;
$display['display_options']['sorts']['created']['expose']['label'] = 'Created';
// Enable combined sort.
$this->setBetterExposedOptions($view, [
'sort' => [
'advanced' => [
'combine' => TRUE,
],
],
]);
// Get the exposed form render array.
$output = $this->getExposedFormRenderArray($view);
// Assert our "sort_bef_combine" contains both sort by and sort order
// options.
$options = $output['sort_bef_combine']['#options'];
$assert = [
'created_ASC' => 'Created Asc',
'created_DESC' => 'Created Desc',
];
// Assert our combined sort options are added.
$this->assertEqual($options, $assert, 'Sort options are combined.');
$view->destroy();
}
/**
* Tests combining and rewriting sort options (sort order and sort by).
*/
public function testCombineRewriteSortOptions() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Ensure our sort "created" is exposed.
$display['display_options']['sorts']['created']['exposed'] = TRUE;
$display['display_options']['sorts']['created']['expose']['label'] = 'Created';
// Enable combined sort and rewrite options.
$this->setBetterExposedOptions($view, [
'sort' => [
'advanced' => [
'combine' => TRUE,
'combine_rewrite' => "Created Desc|down\r\nCreated Asc|up",
],
],
]);
// Get the exposed form render array.
$output = $this->getExposedFormRenderArray($view);
// Assert our "sort_bef_combine" contains both sort by and sort order
// options, and has its options rewritten.
$options = $output['sort_bef_combine']['#options'];
$assert = [
'created_DESC' => 'down',
'created_ASC' => 'up',
];
// Assert our combined sort options are added.
$this->assertEqual($options, $assert, 'Sort options are combined and rewritten.');
$view->destroy();
}
/**
* Tests adding a reset sort option.
*/
public function testResetSortOptions() {
$view = Views::getView('bef_test');
$display = &$view->storage->getDisplay('default');
// Ensure our sort "created" is exposed.
$display['display_options']['sorts']['created']['exposed'] = TRUE;
$display['display_options']['sorts']['created']['expose']['label'] = 'Created';
// Enable combined sort and rewrite options.
$this->setBetterExposedOptions($view, [
'sort' => [
'advanced' => [
'combine' => TRUE,
'reset' => TRUE,
'reset_label' => 'Reset sort',
],
],
]);
// Get the exposed form render array.
$output = $this->getExposedFormRenderArray($view);
// Assert our "sort_bef_combine" contains a reset option at the top.
$options = $output['sort_bef_combine']['#options'];
$assert = [
' ' => 'Reset sort',
'created_ASC' => 'Created Asc',
'created_DESC' => 'Created Desc',
];
// Assert our combined sort options are added.
$this->assertEqual($options, $assert, 'Reset sort option was added.');
$view->destroy();
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Traits;
use Drupal\Component\Utility\NestedArray;
use Drupal\views\ViewExecutable;
/**
* Makes Drupal's test API forward compatible with multiple versions of PHPUnit.
*/
trait BetterExposedFiltersTrait {
/**
* Returns the configured BEF options.
*
* @param \Drupal\views\ViewExecutable $view
* The view object.
*
* @return array
* Array of BEF options.
*/
protected function &getBetterExposedOptions(ViewExecutable $view) {
return $view->storage->getDisplay('default')['display_options']['exposed_form']['options']['bef'];
}
/**
* Merges options into existing BEF configuration.
*
* @param \Drupal\views\ViewExecutable $view
* The view object.
* @param array $options
* The list of options (e.g. ['sort' => ['plugin_id' => 'default']]).
*
* @throws \Drupal\Core\Entity\EntityStorageException
* In case of failures an exception is thrown.
*/
protected function setBetterExposedOptions(ViewExecutable $view, array $options) {
$bef_options = &$this->getBetterExposedOptions($view);
$bef_options = NestedArray::mergeDeep($bef_options, $options);
$view->storage->save();
}
}
<?php
namespace Drupal\Tests\better_exposed_filters\Unit;
use Drupal\better_exposed_filters\BetterExposedFiltersHelper;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Tests\UnitTestCase;
/**
* Tests the helper functions for better exposed filters.
*
* @coversDefaultClass \Drupal\better_exposed_filters\BetterExposedFiltersHelper
*
* @group better_exposed_filters
*/
class BetterExposedFiltersHelperUnitTest extends UnitTestCase {
use StringTranslationTrait;
/**
* Tests options are rewritten correctly.
*
* @dataProvider providerTestRewriteOptions
*
* @covers ::rewriteOptions
*/
public function testRewriteOptions($options, $settings, $expected) {
$actual = BetterExposedFiltersHelper::rewriteOptions($options, $settings);
$this->assertArrayEquals(array_values($expected), array_values($actual));
}
/**
* Data provider for ::testRewriteOptions.
*/
public function providerTestRewriteOptions() {
$data = [];
// Super basic rewrite.
$data[] = [
['foo' => 'bar'],
"bar|baz",
['foo' => 'baz'],
];
// Removes an option.
$data[] = [
['foo' => 'bar'],
"bar|",
[],
];
// An option in the middle is removed -- preserves order.
$data[] = [
['foo' => '1', 'bar' => '2', 'baz' => '3'],
"2|",
['foo' => '1', 'baz' => '3'],
];
// Ensure order is preserved.
$data[] = [
['foo' => '1', 'bar' => '2', 'baz' => '3'],
"2|Two",
['foo' => '1', 'bar' => 'Two', 'baz' => '3'],
];
// No options are replaced.
$data[] = [
['foo' => '1', 'bar' => '2', 'baz' => '3'],
"4|Two",
['foo' => '1', 'bar' => '2', 'baz' => '3'],
];
// All options are replaced.
$data[] = [
['foo' => '1', 'bar' => '2', 'baz' => '3'],
"1|One\n2|Two\n3|Three",
['foo' => 'One', 'bar' => 'Two', 'baz' => 'Three'],
];
return $data;
}
/**
* Tests options are rewritten correctly.
*
* @dataProvider providerTestRewriteReorderOptions
*
* @covers ::rewriteOptions
*/
public function testRewriteReorderOptions($options, $settings, $expected) {
$actual = BetterExposedFiltersHelper::rewriteOptions($options, $settings, TRUE);
$this->assertArrayEquals(array_values($expected), array_values($actual));
}
/**
* Data provider for ::testRewriteReorderOptions.
*/
public function providerTestRewriteReorderOptions() {
$data = [];
// Basic use case.
$data[] = [
['foo' => '1', 'bar' => '2', 'baz' => '3'],
'2|Two',
['bar' => 'Two', 'foo' => '1', 'baz' => '3'],
];
// No option replaced should not change the order.
$data[] = [
['foo' => '1', 'bar' => '2', 'baz' => '3'],
'4|Four',
['foo' => '1', 'bar' => '2', 'baz' => '3'],
];
// Completely reorder options.
$data[] = [
['foo' => '1', 'bar' => '2', 'baz' => '3'],
"3|Three\n2|Two\n1|One",
['baz' => 'Three', 'bar' => 'Two', 'foo' => 'One'],
];
return $data;
}
/**
* Tests options are rewritten correctly.
*
* @dataProvider providerTestRewriteTaxonomy
*
* @covers ::rewriteOptions
*/
public function testRewriteTaxonomy($options, $settings, $expected, $reorder = FALSE) {
$actual = BetterExposedFiltersHelper::rewriteOptions($options, $settings, $reorder);
$this->assertArrayEquals(array_values($expected), array_values($actual));
}
/**
* Data provider for ::testRewriteTaxonomy.
*/
public function providerTestRewriteTaxonomy() {
$data = [];
// Replace a single item, no change in order.
$data[] = [
[
(object) ['option' => [123 => 'term1']],
(object) ['option' => [456 => 'term2']],
(object) ['option' => [789 => 'term3']],
],
"term2|Two",
[
(object) ['option' => [123 => 'term1']],
(object) ['option' => [456 => 'Two']],
(object) ['option' => [789 => 'term3']],
],
];
// Replace all items, no change in order.
$data[] = [
[
(object) ['option' => [123 => 'term1']],
(object) ['option' => [456 => 'term2']],
(object) ['option' => [789 => 'term3']],
],
"term2|Two\nterm3|Three\nterm1|One",
[
(object) ['option' => [123 => 'One']],
(object) ['option' => [456 => 'Two']],
(object) ['option' => [789 => 'Three']],
],
];
// Replace a single item, with change in order.
$data[] = [
[
(object) ['option' => [123 => 'term1']],
(object) ['option' => [456 => 'term2']],
(object) ['option' => [789 => 'term3']],
], "term2|Two",
[
(object) ['option' => [456 => 'Two']],
(object) ['option' => [123 => 'term1']],
(object) ['option' => [789 => 'term3']],
], TRUE,
];
// Replace all items, with change in order.
$data[] = [
[
(object) ['option' => [123 => 'term1']],
(object) ['option' => [456 => 'term2']],
(object) ['option' => [789 => 'term3']],
], "term2|Two\nterm3|Three\nterm1|One",
[
(object) ['option' => [456 => 'Two']],
(object) ['option' => [789 => 'Three']],
(object) ['option' => [123 => 'One']],
], TRUE,
];
return $data;
}
/**
* Tests options are rewritten correctly.
*
* @dataProvider providerTestSortOptions
*
* @covers ::sortOptions
*/
public function testSortOptions($unsorted, $expected) {
$sorted = BetterExposedFiltersHelper::sortOptions($unsorted);
$this->assertArrayEquals(array_values($sorted), array_values($expected));
}
/**
* Data provider for ::testSortOptions.
*/
public function providerTestSortOptions() {
// Data providers run before ::setUp. We rely on the stringTranslationTrait
// for some of our option values so call it here instead.
$this->stringTranslation = $this->getStringTranslationStub();
$data = [];
// List of strings.
$data[] = [
[
'e',
'a',
'b',
'd',
'c',
], [
'a',
'b',
'c',
'd',
'e',
],
];
// List of mixed values.
$data[] = [
[
'1',
'a',
'2',
'b',
'3',
], [
'1',
'2',
'3',
'a',
'b',
],
];
// List of taxonomy terms.
$data[] = [
[
(object) ['option' => [555 => 'term5']],
(object) ['option' => [222 => 'term2']],
(object) ['option' => [444 => 'term4']],
(object) ['option' => [333 => 'term3']],
(object) ['option' => [111 => 'term1']],
], [
(object) ['option' => [111 => 'term1']],
(object) ['option' => [222 => 'term2']],
(object) ['option' => [333 => 'term3']],
(object) ['option' => [444 => 'term4']],
(object) ['option' => [555 => 'term5']],
],
];
return $data;
}
/**
* Tests options are rewritten correctly.
*
* @dataProvider providerTestSortNestedOptions
*
* @covers ::sortNestedOptions
*/
public function testSortNestedOptions($unsorted, $expected) {
$sorted = BetterExposedFiltersHelper::sortNestedOptions($unsorted);
$this->assertArrayEquals(array_values($sorted), array_values($expected));
}
/**
* Data provider for ::testSortNestedOptions.
*/
public function providerTestSortNestedOptions() {
// Data providers run before ::setUp. We rely on the stringTranslationTrait
// for some of our option values so call it here instead.
$this->stringTranslation = $this->getStringTranslationStub();
$data = [];
// List of nested taxonomy terms.
$data[] = [
[
(object) ['option' => [2303 => 'United States']],
(object) ['option' => [2311 => '-Washington']],
(object) ['option' => [2312 => '--Seattle']],
(object) ['option' => [2313 => '--Spokane']],
(object) ['option' => [2314 => '--Walla Walla']],
(object) ['option' => [2304 => '-California']],
(object) ['option' => [2307 => '--Santa Barbara']],
(object) ['option' => [2306 => '--San Diego']],
(object) ['option' => [2305 => '--San Francisco']],
(object) ['option' => [2308 => '-Oregon']],
(object) ['option' => [2310 => '--Eugene']],
(object) ['option' => [2309 => '--Portland']],
(object) ['option' => [2324 => 'Mexico']],
(object) ['option' => [2315 => 'Canada']],
(object) ['option' => [2316 => '-British Columbia']],
(object) ['option' => [2319 => '--Whistler']],
(object) ['option' => [2317 => '--Vancouver']],
(object) ['option' => [2318 => '--Victoria']],
(object) ['option' => [2320 => '-Alberta']],
(object) ['option' => [2321 => '--Calgary']],
(object) ['option' => [2323 => '--Lake Louise']],
(object) ['option' => [2322 => '--Edmonton']],
], [
(object) ['option' => [2315 => 'Canada']],
(object) ['option' => [2320 => '-Alberta']],
(object) ['option' => [2321 => '--Calgary']],
(object) ['option' => [2322 => '--Edmonton']],
(object) ['option' => [2323 => '--Lake Louise']],
(object) ['option' => [2316 => '-British Columbia']],
(object) ['option' => [2317 => '--Vancouver']],
(object) ['option' => [2318 => '--Victoria']],
(object) ['option' => [2319 => '--Whistler']],
(object) ['option' => [2324 => 'Mexico']],
(object) ['option' => [2303 => 'United States']],
(object) ['option' => [2304 => '-California']],
(object) ['option' => [2306 => '--San Diego']],
(object) ['option' => [2305 => '--San Francisco']],
(object) ['option' => [2307 => '--Santa Barbara']],
(object) ['option' => [2308 => '-Oregon']],
(object) ['option' => [2310 => '--Eugene']],
(object) ['option' => [2309 => '--Portland']],
(object) ['option' => [2311 => '-Washington']],
(object) ['option' => [2312 => '--Seattle']],
(object) ['option' => [2313 => '--Spokane']],
(object) ['option' => [2314 => '--Walla Walla']],
],
];
return $data;
}
}
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