filter_harmonizer.module in Views Filter Harmonizer 8
Same filename and directory in other branches
Harmonizes contextual with regular filter when both exist on the same View.
File
filter_harmonizer.moduleView source
<?php
use Drupal\Core\Routing\RouteMatch;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\argument\ArgumentPluginBase;
use Drupal\views\Plugin\views\filter\FilterPluginBase;
use Drupal\Core\Render\Element\Form;
/**
* @file
* filter_harmonizer.module
*
* Harmonizes contextual with regular filter when both exist on the same View.
*/
// Set by filter_harmonizer_views_pre_build() implementation and read in
// filter_harmonizer_page_attachments()
$filter_harmonizer_filter_pairs = [];
/**
* Implements hook_views_pre_build().
*/
function filter_harmonizer_views_pre_build(ViewExecutable $view) {
global $filter_harmonizer_filter_pairs;
$filter_harmonizer_filter_pairs = [];
// If this view has both a regular filter and a contextual filter argument,
// and is requested to be harmonized on the settings page, then harmonize it
// and return filter pair data for later use during this this HTTP request.
if (!($config = Drupal::config('filter_harmonizer.settings'))) {
return;
}
$harmonized_view_ids = $config
->get('filter_harmonizer_harmonized_view_ids') ?? [];
if (!in_array($view
->id(), $harmonized_view_ids)) {
return [];
}
$filter_harmonizer_filter_pairs = filter_harmonizer_harmonize_and_record_filter_pairs($view);
}
/**
* When a View on a page is first loaded and if a context filter is present,
* we obey any associated /arg1/arg2/.. on the browser URL/address bar.
* Then, if regular exposed filters are present too for fields by the same
* names, and their form values are submitted, we obey those submitted values,
* discarding the contextual filter values used on initial page load.
*
* For clarity and visual feedback to the user, we support the option to
* reflect, on the regular exposed filter form, the equivalent of the contextual
* argument value as appearing in the URL.
* Conversely, after exposed regular form values were selected and submitted,
* there's the option to reflect this in the browser address bar URL, as
* contextual arguments. Using contextual URL arguments is more concise then
* the verbose URL query string format, i.e. "?...".
*
* The page with its filter settings can thus be concisely shared by copying the
* browser address bar and sending it in emails and on social media.
*
* @param ViewExecutable $view
* The View. Its contextual or regular filters may be set in this call.
* @return array
* An array with info about regular+contextual filter pairs on the View.
*/
function filter_harmonizer_harmonize_and_record_filter_pairs(ViewExecutable $view) {
$view_id = $view
->id();
$display = $view
->getDisplay();
// e.g. 'block' or 'page' object
$regular_filter_inputs = $display
->getPlugin('exposed_form') ? $view
->getExposedInput() : [];
$display_id = $display->display['id'];
// e.g. 'block_5' or 'page_1'
$request = Drupal::request();
$filter_pairs = [];
//$pager = $display->getPlugin('pager');
//$is_paged = !($pager instanceof Drupal\views\Plugin\views\pager\None);
// Examine the current display being executed on the View and see if it has
// both a non-empty contextual and a non-empty (exposed) regular filter,
// operating on the same field. If it does "harmonize".
// Also return essential filter data for later use eg. in
// hook_page_attachments(), which is required to add JavaScript to the page,
// while not having direct access to the View filters on the page.
$regular_filters = $display
->getHandlers('filter');
$contextual_filters = $display
->getHandlers('argument');
$active_regular_filter_count = 0;
$i = -1;
foreach ($contextual_filters as $field_name => $contextual_filter) {
// Temporary store filter values for easy access during this request.
$filter_pair =& $filter_pairs[$view_id][$display_id][$field_name];
// Record $contextual_filter->argument (from URL) as we need it now.
// Normally this is done later in ViewExecutable::build($display_id).
// We need to record all contextual filters, even those that do not have
// a companion regular filter, in order to correctly apply URL argument
// substitution, using '/all' for skipped filters.
$i++;
if ($is_contextual_filter_present = isset($view->args[$i])) {
$contextual_filter->position = $i;
$contextual_filter
->setArgument($view->args[$i]);
$filter_pair['contextual']['argument'] = $contextual_filter->argument;
}
$filter_pair['contextual']['exception'] = $contextual_filter->options['exception']['value'];
// eg. 'all';
// Collect the companion regular filter value by the same name, if present.
if (isset($regular_filters[$field_name])) {
$regular_filter = $regular_filters[$field_name];
$filter_pair['field_label'] = $regular_filter->definition['title'];
$field_alias = $regular_filter->options['exposed'] ? $regular_filter->options['expose']['identifier'] : $field_name;
$filter_pair['regular']['alias'] = $field_alias;
$is_regular_filter_active = isset($regular_filter_inputs[$field_alias]);
if ($is_regular_filter_active) {
$active_regular_filter_count++;
$filter_pair['regular']['value'] = $regular_filter->value = $regular_filter_inputs[$field_alias];
$filter_pair['regular']['stringified'] = filter_harmonizer_stringify_regular_filter($regular_filter, $contextual_filter);
}
if ($is_contextual_filter_present) {
if ($is_regular_filter_active) {
// User has submitted regular filter values. So we have two competing
// filters. Let's harmonize! Deactivate contextual now, before the
// WHERE clause is generated that stuffs things up.
unset($view->args[$i]);
// Title now no longer reflects contextual filter value.
// Better to have a blank title than a wrong title...
$contextual_filter->options['title'] = '';
}
elseif (Drupal::config('filter_harmonizer.settings')
->get('filter_harmonizer_contextual_args_in_exposed_form')) {
// No regular filter value set, get it from contextual arg.
$regular_filter_inputs[$field_alias] = filter_harmonizer_formalize_contextual_arg($regular_filter, $contextual_filter);
}
}
}
}
if (!$active_regular_filter_count && ($regular_filter_inputs = array_filter($regular_filter_inputs))) {
$view
->setExposedInput($regular_filter_inputs);
}
return $filter_pairs;
}
/**
* Returns TRUE when the supplied regular filter info is empty or incomplete.
*
* By default filter $value is considered empty when it:
* -- equals NULL or the empty string '' or an empty array, or
* -- when all of its array elements
* - equal NULL or '' or an empty array, or
* - are arrays with elements that equal NULL or '' or an empty array
*
* @param FilterPluginBase $filter
* Contains the (limit) value and operator.
*/
function filter_harmonizer_is_empty(FilterPluginBase $filter) {
if (!$filter || !isset($filter->value)) {
return TRUE;
}
// This covers most simple regular filters...
$is_empty = $filter->value == '' || $filter->value === [] || empty($filter->no_operator) && empty($filter->operator);
// And here's for the more complex ones, e.g. multi-valued ones.
if (!$is_empty && is_array($filter->value)) {
foreach ($filter->value as $key => $val) {
if ($is_empty = !isset($val) || $val === [] || $val == '') {
break;
}
}
}
// Let other modules modify the meaning of $is_empty before we return it.
// Example: Geofield Proximity filter.
Drupal::moduleHandler()
->invokeAll('filter_harmonizer_is_empty', [
&$is_empty,
$filter,
]);
return $is_empty;
}
/**
* Takes a regular filter and outputs its filter value(s) as a string for later
* use as a contextual argument in the browser URL.
* For example a GeofieldProximityFilter value may return "-37.8,144.9<=100km".
*
* @param FilterPluginBase $regular_filter.
* The regular filter handler with its (exposed) values and optional operator.
* @param ArgumentPluginBase $contextual_filter.
* The contextual filter whose arg will be calculated from the regular value.
*
* @return string
* The contextual argument string for use on the browser address bar.
*/
function filter_harmonizer_stringify_regular_filter(FilterPluginBase $regular_filter, ArgumentPluginBase $contextual_filter) {
if (filter_harmonizer_is_empty($regular_filter)) {
return $contextual_filter->argument ?: $contextual_filter->options['exception']['value'];
}
$value = Drupal::moduleHandler()
->invokeAll('filter_harmonizer_stringify_regular_filter', [
$regular_filter,
]);
if (empty($value) && $value != 0) {
$value = $regular_filter->value;
}
if (is_array($value)) {
$isAnd = $regular_filter->operator == 'and';
$delimiter = empty($isAnd) ? '+' : ',';
$value = implode($delimiter, $value);
}
return $value;
}
/**
* Takes a contextual filter arg and outputs it as form value(s) to later
* populate the exposed form belonging to a companion regular filter.
*
* @param FilterPluginBase $regular_filter
* The regular filter handler whose (exposed) form will be populated from the
* contextual filter.
* @param ArgumentPluginBase $contextual_filter
* The contextual filter whose arg will be used to return regular filter form
* values.
*
* @return string|array
* A single number or string or array of values to populate the regular filter
*/
function filter_harmonizer_formalize_contextual_arg(FilterPluginBase $regular_filter, ArgumentPluginBase $contextual_filter) {
$values = Drupal::moduleHandler()
->invokeAll('filter_harmonizer_formalize_contextual_arg', [
$contextual_filter,
$regular_filter,
]);
if (empty($values)) {
// If no special implementation has provided the contextual argument in
// a format we can use on the regular filter form, then use
// $contextual_filter_argument as is.
if ($contextual_filter->argument !== $contextual_filter->options['exception']['value']) {
$values = [];
$and_ed = explode(',', $contextual_filter->argument);
foreach ($and_ed as $value) {
$or_ed = explode('+', $value);
// If '+' doesn't work, try space instead. This is how '+' usually gets
// converted into $contextual_filter->argument.
if (count($or_ed) == 1) {
$or_ed = explode(' ', $value);
}
$values = array_merge($values, $or_ed);
}
}
}
// If the contextual multiplicity is at odds with the regular filter, make
// an attempt to sort out the mismatch.
$is_multi_valued = !empty($regular_filter->options['expose']['multiple']);
if ($is_multi_valued) {
if (!is_array($values)) {
// Convert the single value into an array (ie. a multi-value).
$values = [
$values,
];
}
}
return $values;
}
/**
* Implements hook_page_attachments().
*
* This is here to implement the feature that reflects the regular filter
* choice on the browser address bar, using contextual filters, rather than
* the "?FIELD_NAME1=VALUE1&FIELD_NAME2=VALUE2" format.
*/
function filter_harmonizer_page_attachments(array &$attachments) {
global $filter_harmonizer_filter_pairs;
// This is only set if there is a View active on the current page.
if (empty($filter_harmonizer_filter_pairs)) {
return;
}
if (!Drupal::config('filter_harmonizer.settings')
->get('filter_harmonizer_regular_filter_values_in_address_bar')) {
return;
}
// If an (exposed) regular filter was submitted, visually reflect that in the
// browser address bar.
$request = Drupal::request();
$route_path = RouteMatch::createFromRequest($request)
->getRouteObject()
->getPath();
$qs = urldecode($request
->getQueryString());
foreach ($filter_harmonizer_filter_pairs as $displays) {
foreach ($displays as $filters) {
$arg_values = [];
$regular_filter_count = 0;
foreach ($filters as $field_name => $filter_pair) {
if (isset($filter_pair['regular'])) {
$regular_filter_count++;
}
// If regular filter is empty, take the contextual value (or 'all').
$arg_values[$field_name] = isset($filter_pair['regular']['value']) ? isset($filter_pair['regular']['stringified']) ? $filter_pair['regular']['stringified'] : $filter_pair['contextual']['exception'] : (isset($filter_pair['contextual']['argument']) ? $filter_pair['contextual']['argument'] : $filter_pair['contextual']['exception']);
}
if (!$regular_filter_count) {
// If we have no active regular filters on this display, then we
// don't have to update the URL path.
continue;
}
// If the number of /{arg} occurrences in $route_path is not equal to the
// number of $arg_values we can move to the next display.
if ($match_count = preg_match_all('/\\/{[^\\/.]+}/', $route_path, $matches)) {
$matches = reset($matches);
// used below
}
if ($match_count != count($arg_values)) {
continue;
}
$path = $route_path;
// Now substitute URL, but don't add superfluous trailing "/all"'s.
// It is assumed that contextual arguments appear in order on the URL.
// Start at the rear.
$i = count($arg_values) - 1;
foreach (array_reverse($arg_values) as $field_name => $value) {
if (empty($done_exc) && $value == $filters[$field_name]['contextual']['exception']) {
// Avoid trailing "/all". Wipe the arg.
$path = str_replace($matches[$i], '', $path);
}
else {
// A non-trailing /all or "normal" value: replace it. Keep the '/'.
$done_exc = TRUE;
$path = str_replace($matches[$i], "/{$value}", $path);
}
// The Views module appends the regular filter values as rather ugly
// query-parameters. Example: ?field_size_value[]=XL
// These query parameters are no longer required because we've just
// put the equivalent values in the URL as contextual arguments.
if (isset($filters[$field_name]['regular'])) {
$field_alias = $filters[$field_name]['regular']['alias'];
$pattern = '/' . $field_alias . '\\[.*\\]=.+[&]?/';
// Remove the query parameter(s) from the query string.
$qs = preg_replace($pattern, '', $qs);
}
$i--;
}
}
}
if (isset($path) && $path !== $route_path) {
$path_plus_qs = empty($qs) ? $path : $path . '?' . $qs;
$attachments['#attached']['drupalSettings']['browser_url'] = $request
->getBasePath() . $path_plus_qs;
// Let's not forget to include the few lines of JS that make this happen!
$attachments['#attached']['library'][] = 'filter_harmonizer/filter_harmonizer';
}
}
if (Drupal::moduleHandler()
->moduleExists('geofield')) {
include_once 'includes/filter_harmonizer_for_geofield.inc';
}
Functions
Name | Description |
---|---|
filter_harmonizer_formalize_contextual_arg | Takes a contextual filter arg and outputs it as form value(s) to later populate the exposed form belonging to a companion regular filter. |
filter_harmonizer_harmonize_and_record_filter_pairs | When a View on a page is first loaded and if a context filter is present, we obey any associated /arg1/arg2/.. on the browser URL/address bar. Then, if regular exposed filters are present too for fields by the same names, and their form values are… |
filter_harmonizer_is_empty | Returns TRUE when the supplied regular filter info is empty or incomplete. |
filter_harmonizer_page_attachments | Implements hook_page_attachments(). |
filter_harmonizer_stringify_regular_filter | Takes a regular filter and outputs its filter value(s) as a string for later use as a contextual argument in the browser URL. For example a GeofieldProximityFilter value may return "-37.8,144.9<=100km". |
filter_harmonizer_views_pre_build | Implements hook_views_pre_build(). |