Commit 632f9547 authored by mollux's avatar mollux Committed by borisson_

Issue #2755663 by borisson_, ransomweaver, mollux, Nick_vh, nickdaelemans,...

Issue #2755663 by borisson_, ransomweaver, mollux, Nick_vh, nickdaelemans, Shven: Add a solid slider and range widget base
parent b7cbb72b
......@@ -5,6 +5,10 @@ CONTENTS OF THIS FILE
* Configuration
* FAQ
INTRODUCTION
------------
Todo
REQUIREMENTS
------------
No other modules required, we're supporting drupal core as a source for creating
......@@ -14,8 +18,8 @@ tested.
INSTALLATION
------------
* Install as you would normally install a contributed drupal module. See:
https://www.drupal.org/docs/8/extending-drupal-8/installing-contributed-modules-find-import-enable-configure-drupal-8
for further information.
https://drupal.org/documentation/install/modules-themes/modules-7
for further information.
CONFIGURATION
-------------
......@@ -54,7 +58,6 @@ further details see: https://www.drupal.org/node/2834730
FAQ
---
Q: Why do the facets disappear after a refresh.
A: We don't support cached views, change the view to disable caching.
......
CONTENTS OF THIS FILE
---------------------
* Installation
* FAQ
Installation
------------
* If you want to use the sliders, you need to add the Slider pips jquery
plugin:
- create the /libraries/jquery-ui-slider-pips folder.
- download the following files from
https://github.com/simeydotme/jQuery-ui-Slider-Pips/tree/v1.11.3/dist
- jquery-ui-slider-pips.min.js
- jquery-ui-slider-pips.min.css
You can find more information about this jquery plugin on
http://simeydotme.github.io/jQuery-ui-Slider-Pips/
FAQ
---
Q: Why is this in a submodule?
A: We wanted to add a requirements message when the library was not installed,
to give a good experience when installing the module. We didn't want everyone
to have to install the library though.
# Config schema for slider, you can find the implementation in
# Drupal\facets_range\Plugin\facets\widget\SliderWidget.
facet.widget.config.slider:
type: facet.widget.default_config
label: 'List of range widget configuration'
mapping:
prefix:
type: string
label: 'Prefix'
suffix:
type: string
label: 'Suffix'
min_type:
type: string
label: 'Minimum type'
min_value:
type: float
label: 'Minimum value'
max_type:
type: string
label: 'Maximum type'
max_value:
type: float
label: 'Maximum value'
step:
type: float
label: 'Step'
.facet-slider {
margin-top: 40px;
}
.facet-slider.ui-slider-float .ui-slider-tip {
visibility: visible;
opacity: 1;
top: -30px;
}
name: 'Facets Range Widget'
type: module
description: 'Provides a range widget and solid slider'
core: 8.x
package: Search
dependencies:
- facets:facets
test_dependencies:
- search_api:search_api
- facets:facets
- drupal:views
jquery.ui.slider.pips:
remote: http://simeydotme.github.io/jQuery-ui-Slider-Pips/
version: v1.11.3
license:
name: MIT
url: https://github.com/simeydotme/jQuery-ui-Slider-Pips/blob/v1.11.3/README.md
gpl-compatible: true
js:
/libraries/jquery-ui-slider-pips/jquery-ui-slider-pips.min.js: { minified: true }
css:
component:
/libraries/jquery-ui-slider-pips/jquery-ui-slider-pips.min.css: { minified: true }
dependencies:
- core/jquery.ui.slider
slider:
version: VERSION
js:
js/slider.js: {}
css:
component:
css/slider.css: {}
dependencies:
- core/drupal
- core/drupalSettings
- core/jquery.once
- facets_range_widget/jquery.ui.slider.pips
/**
* @file
* Provides the slider functionality.
*/
(function ($) {
"use strict";
Drupal.facets = Drupal.facets || {};
Drupal.behaviors.facet_slider = {
attach: function (context, settings) {
if (settings.facets != undefined && settings.facets.sliders != undefined) {
$.each(settings.facets.sliders, function (facet, settings) {
Drupal.facets.addSlider(facet, settings);
})
}
}
};
Drupal.facets.addSlider = function (facet, settings) {
var defaults = {
stop: function(event, ui) {
if (settings.range) {
window.location.href = settings.url.replace('__range_slider_min__', ui.values[0]).replace('__range_slider_max__', ui.values[1]);
}
else {
window.location.href = settings.urls['f_' + ui.value];
}
}
};
$.extend(defaults, settings);
$("#" + facet).slider(defaults)
.slider("pips", {
prefix: settings.prefix,
suffix: settings.suffix
})
.slider("float", {
prefix: settings.prefix,
suffix: settings.suffix,
labels: settings.labels
})
}
})(jQuery);
<?php
namespace Drupal\facets_range_widget\Plugin\facets\processor;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\PreQueryProcessorInterface;
/**
* Provides a processor that adds all range values between an min and max range.
*
* @FacetsProcessor(
* id = "range_slider",
* label = @Translation("Range slider"),
* description = @Translation("Add range results for all the steps beteen min and max range."),
* stages = {
* "pre_query" = 5,
* "post_query" = 5,
* "build" = 5
* }
* )
*/
class RangeSliderProcessor extends SliderProcessor implements PreQueryProcessorInterface, BuildProcessorInterface {
/**
* {@inheritdoc}
*/
public function preQuery(FacetInterface $facet) {
$active_items = $facet->getActiveItems();
array_walk($active_items, function (&$item) {
if (preg_match('/\(min:((?:-)?[\d\.]+),max:((?:-)?[\d\.]+)\)/i', $item, $matches)) {
$item = array($matches[1], $matches[2]);
}
else {
$item = NULL;
}
});
$facet->setActiveItems($active_items);
}
/**
* {@inheritdoc}
*/
public function build(FacetInterface $facet, array $results) {
/** @var \Drupal\facets\Plugin\facets\processor\UrlProcessorHandler $url_processor_handler */
$url_processor_handler = $facet->getProcessors()['url_processor_handler'];
$url_processor = $url_processor_handler->getProcessor();
$filter_key = $url_processor->getFilterKey();
/** @var \Drupal\facets\Result\ResultInterface[] $results */
foreach ($results as &$result) {
$url = $result->getUrl();
$query = $url->getOption('query');
// Remove all the query filters for the field of the facet.
foreach ($query[$filter_key] as $id => $filter) {
if (strpos($filter . $url_processor->getSeparator(), $facet->getUrlAlias()) === 0) {
unset($query[$filter_key][$id]);
}
}
// Add one generic query filter with the min and max placeholder.
$query[$filter_key][] = $facet->getUrlAlias() . $url_processor->getSeparator() . '(min:__range_slider_min__,max:__range_slider_max__)';
$url->setOption('query', $query);
$result->setUrl($url);
}
return $results;
}
}
<?php
namespace Drupal\facets_range_widget\Plugin\facets\processor;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\PostQueryProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
use Drupal\facets\Result\Result;
/**
* Provides a processor that adds all values between an min and max range.
*
* @FacetsProcessor(
* id = "slider",
* label = @Translation("Slider"),
* description = @Translation("Add results for all the steps beteen min and max range."),
* stages = {
* "post_query" = 5
* }
* )
*/
class SliderProcessor extends ProcessorPluginBase implements PostQueryProcessorInterface {
/**
* {@inheritdoc}
*/
public function postQuery(FacetInterface $facet) {
$widget = $facet->getWidgetInstance();
$config = $widget->getConfiguration();
$simple_results = [];
// Generate all the "results" between min and max, with the configured step.
foreach ($facet->getResults() as $result) {
$simple_results['f_' . (float) $result->getRawValue()] = [
'value' => (float) $result->getRawValue(),
'count' => (int) $result->getCount(),
];
}
if ($config['min_type'] == 'fixed') {
$min = $config['min_value'];
$max = $config['max_value'];
}
else {
$min = reset($simple_results)['value'];
$max = end($simple_results)['value'];
}
$step = $config['step'];
// Creates an array of all results between min and max by the step from the
// configuration.
$new_results = [];
for ($i = $min; $i <= $max; $i += $step) {
$count = isset($simple_results['f_' . $i]) ? $simple_results['f_' . $i]['count'] : 0;
$new_results[] = new Result((float) $i, (float) $i, $count);
}
// Overwrite the current facet values with the generated results.
$facet->setResults($new_results);
}
}
<?php
namespace Drupal\facets_range_widget\Plugin\facets\widget;
use Drupal\facets\FacetInterface;
/**
* The range slider widget.
*
* @FacetsWidget(
* id = "range_slider",
* label = @Translation("Range slider"),
* description = @Translation("A widget that shows a range slider."),
* )
*/
class RangeSliderWidget extends SliderWidget {
/**
* {@inheritdoc}
*/
public function build(FacetInterface $facet) {
$build = parent::build($facet);
$active = $facet->getActiveItems();
$facet_settings = &$build['#attached']['drupalSettings']['facets']['sliders'][$facet->id()];
$facet_settings['range'] = TRUE;
$facet_settings['url'] = reset($facet_settings['urls']);
unset($facet_settings['value']);
unset($facet_settings['urls']);
$min = $facet_settings['min'];
$max = $facet_settings['max'];
$facet_settings['values'] = [isset($active[0][0]) ? (float) $active[0][0] : $min, isset($active[0][1]) ? (float) $active[0][1] : $max];
return $build;
}
/**
* {@inheritdoc}
*/
public function isPropertyRequired($name, $type) {
if ($name === 'slider' && $type === 'processors') {
return TRUE;
}
if ($name === 'show_only_one_result' && $type === 'settings') {
return TRUE;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getQueryType(array $query_types) {
return $query_types['range'];
}
}
<?php
namespace Drupal\facets_range_widget\Plugin\facets\widget;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Widget\WidgetPluginBase;
/**
* The slider widget.
*
* @FacetsWidget(
* id = "slider",
* label = @Translation("Slider"),
* description = @Translation("A widget that shows a slider."),
* )
*/
class SliderWidget extends WidgetPluginBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'prefix' => '',
'suffix' => '',
'min_type' => 'search_result',
'min_value' => 0,
'max_type' => 'search_result',
'max_value' => 10,
'step' => 1,
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function build(FacetInterface $facet) {
$results = $facet->getResults();
ksort($results);
$show_numbers = $facet->getWidgetInstance()->getConfiguration()['show_numbers'];
$urls = [];
$labels = [];
foreach ($results as $result) {
$urls['f_' . $result->getRawValue()] = $result->getUrl()->toString();
$labels[] = $result->getDisplayValue() . ($show_numbers ? ' (' . $result->getCount() . ')' : '');
}
$min = (float) min($results)->getRawValue();
$max = (float) max($results)->getRawValue();
$build['slider'] = [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => [
'class' => ['facet-slider'],
'id' => $facet->id(),
],
];
$active = $facet->getActiveItems();
$build['#attached']['library'][] = 'facets_range_widget/slider';
$build['#attached']['drupalSettings']['facets']['sliders'][$facet->id()] = [
'min' => $min,
'max' => $max,
'value' => isset($active[0]) ? (float) $active[0] : '',
'urls' => $urls,
'prefix' => $this->getConfiguration()['prefix'],
'suffix' => $this->getConfiguration()['suffix'],
'step' => $this->getConfiguration()['step'],
'labels' => $labels,
];
return $build;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
$config = $this->getConfiguration();
$form = parent::buildConfigurationForm($form, $form_state, $facet);
$form['prefix'] = [
'#type' => 'textfield',
'#title' => $this->t('Value prefix'),
'#size' => 5,
'#default_value' => $config['prefix'],
];
$form['suffix'] = [
'#type' => 'textfield',
'#title' => $this->t('Value suffix'),
'#size' => 5,
'#default_value' => $config['suffix'],
];
$form['min_type'] = [
'#type' => 'radios',
'#options' => [
'fixed' => $this->t('Fixed value'),
'search_result' => $this->t('Based on search result'),
],
'#title' => $this->t('Minimum value type'),
'#default_value' => $config['min_type'],
];
$form['min_value'] = [
'#type' => 'number',
'#title' => $this->t('Minimum value'),
'#default_value' => $config['min_value'],
'#size' => 10,
'#states' => [
'visible' => [
'input[name="widget_config[min_type]"]' => ['value' => 'fixed'],
],
],
];
$form['max_type'] = [
'#type' => 'radios',
'#options' => [
'fixed' => $this->t('Fixed value'),
'search_result' => $this->t('Based on search result'),
],
'#title' => $this->t('Maximum value type'),
'#default_value' => $config['max_type'],
];
$form['max_value'] = [
'#type' => 'number',
'#title' => $this->t('Maximum value'),
'#default_value' => $config['max_value'],
'#size' => 5,
'#states' => [
'visible' => [
'input[name="widget_config[max_type]"]' => ['value' => 'fixed'],
],
],
];
$form['step'] = [
'#type' => 'number',
'#step' => 0.001,
'#title' => $this->t('slider step'),
'#default_value' => $config['step'],
'#size' => 2,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function getQueryType(array $query_types) {
return $query_types['string'];
}
/**
* {@inheritdoc}
*/
public function isPropertyRequired($name, $type) {
if ($name === 'slider' && $type === 'processors') {
return TRUE;
}
if ($name === 'show_only_one_result' && $type === 'settings') {
return TRUE;
}
return FALSE;
}
}
<?php
namespace Drupal\Tests\facets_range_widget\Functional;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\search_api\Item\Field;
use Drupal\Tests\facets\Functional\FacetsTestBase;
/**
* Tests the overall functionality of the Facets admin UI.
*
* @group facets
*/
class SliderIntegrationTest extends FacetsTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'views',
'node',
'search_api',
'facets',
'facets_range_widget',
'block',
'facets_search_api_dependency',
'facets_query_processor',
];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
$this->setUpExampleStructure();
$this->insertExampleContent();
$this->assertEquals(5, $this->indexItems($this->indexId), '5 items were indexed.');
}
/**
* Tests slider widget.
*/
public function testSliderWidget() {
$this->createIntegerField();
$id = 'owl';
$name = 'Owl widget.';
$this->createFacet($name, $id, 'field_integer');
$this->drupalGet('admin/config/search/facets/' . $id . '/edit');
$this->assertSession()->checkboxNotChecked('edit-facet-settings-slider-status');
$this->drupalPostForm(NULL, ['widget' => 'slider'], 'Configure widget');
$this->drupalPostForm(NULL, ['widget' => 'slider'], 'Save');
$this->assertSession()->checkboxChecked('edit-facet-settings-slider-status');
$this->drupalGet('search-api-test-fulltext');
$this->assertFacetBlocksAppear();
$this->assertSession()->pageTextContains('Displaying 12 search results');
// Change the facet block.
$url = Url::fromUserInput('/search-api-test-fulltext', ['query' => ['f[0]' => 'owl:2']]);
$this->drupalGet($url->setAbsolute()->toString());
// Check that the results have changed to the correct amount of results.
$this->assertSession()->pageTextContains('Displaying 1 search results');
$this->assertSession()->pageTextContains('foo bar baz 2');
// Change the facet block.
$url = Url::fromUserInput('/search-api-test-fulltext', ['query' => ['f[0]' => 'owl:4']]);
$this->drupalGet($url->setAbsolute()->toString());
// Check that the results have changed to the correct amount of results.
$this->assertSession()->pageTextContains('Displaying 1 search results');
$this->assertSession()->pageTextContains('foo bar baz 4');
}
/**
* Create integer field.
*/
protected function createIntegerField() {
$index = $this->getIndex();
// Create integer field.
$field_name = 'field_integer';
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test_mulrev_changed',
'type' => 'integer',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'item',
]);
$field->save();
// Create the field for search api.
$intfield = new Field($index, $field_name);
$intfield->setType('integer');
$intfield->setPropertyPath($field_name);
$intfield->setDatasourceId('entity:entity_test_mulrev_changed');
$intfield->setLabel('IntegerField');
// Add to field to the index.
$index->addField($intfield);
$index->save();
$this->indexItems($this->indexId);
// Add new entities.
$entity_test_storage = \Drupal::entityTypeManager()