Commit a12d307b authored by StryKaizer's avatar StryKaizer Committed by borisson_

Issue #2807333 by StryKaizer, borisson_, ndrake86, dermario, mpp, marthinal,...

Issue #2807333 by StryKaizer, borisson_, ndrake86, dermario, mpp, marthinal, spydimarri, aspilicious: Implement hierarchical structures in facets
parent b61159f9
......@@ -32,6 +32,15 @@ facets.facet.*:
exclude:
type: boolean
label: 'Exclude'
use_hierarchy:
type: boolean
label: 'Use hierarchy'
expand_hierarchy:
type: boolean
label: 'Expand hierarchy'
enable_parent_when_child_gets_disabled:
type: boolean
label: 'Enable parent when child gets disabled'
widget:
type: mapping
label: 'Facet widget'
......
......@@ -14,6 +14,14 @@ plugin.plugin_configuration.facets_processor.display_value_widget_order:
type: string
label: sort order
plugin.plugin_configuration.facets_processor.translate_entity:
type: mapping
label: 'Translate entity'
mapping:
sort:
type: boolean
label: translate entity
plugin.plugin_configuration.facets_processor.exclude_specified_items:
type: mapping
label: 'Exclude specified items'
......
.block-facets ul ul li {
margin-left: 10px;
}
......@@ -30,6 +30,12 @@ drupal.facets.checkbox-widget:
- core/drupal
- core/jquery.once
drupal.facets.hierarchical:
version: VERSION
css:
theme:
css/hierarchical.css: {}
drupal.facets.dropdown-widget:
version: VERSION
js:
......
......@@ -14,6 +14,9 @@ services:
plugin.manager.facets.url_processor:
class: Drupal\facets\UrlProcessor\UrlProcessorPluginManager
parent: default_plugin_manager
plugin.manager.facets.hierarchy:
class: Drupal\facets\Hierarchy\HierarchyPluginManager
parent: default_plugin_manager
facets.manager:
class: Drupal\facets\FacetManager\DefaultFacetManager
arguments:
......
......@@ -21,6 +21,8 @@
// Find all checkbox facet links and give them a checkbox.
var $links = $('.js-facets-checkbox-links .facet-item a');
$links.once('facets-checkbox-transform').each(Drupal.facets.makeCheckbox);
// Set indeterminate value on parents having an active trail
$('.facet-item--expanded.facet-item--active-trail > input').prop("indeterminate", true);
};
/**
......
......@@ -114,7 +114,8 @@ class CoreNodeSearchDate extends QueryTypePluginBase {
$result->setActiveState(TRUE);
// Sets the children for the current parent..
if ($parent) {
$parent->setChildren($result);
$children = array_merge($parent->getChildren(), [$result]);
$parent->setChildren($children);
}
else {
$parent = $parent_facet_results[] = $result;
......@@ -180,7 +181,8 @@ class CoreNodeSearchDate extends QueryTypePluginBase {
$parent = end($parent_facet_results);
if ($parent instanceof ResultInterface) {
foreach ($facet_results as $result) {
$parent->setChildren($result);
$children = array_merge($parent->getChildren(), [$result]);
$parent->setChildren($children);
$this->facet->setResults($parent_facet_results);
}
}
......
<?php
namespace Drupal\facets\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Facets Hierarchy annotation.
*
* @see \Drupal\facets\Hierarchy\HierarchyPluginManager
* @see plugin_api
*
* @ingroup plugin_api
*
* @Annotation
*/
class FacetsHierarchy extends Plugin {
/**
* The Hierarchy plugin id.
*
* @var string
*/
public $id;
/**
* The human-readable name of the Hierarchy plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The Hierarchy description.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
}
......@@ -41,6 +41,9 @@ use Drupal\facets\FacetInterface;
* "facet_source_id",
* "widget",
* "query_operator",
* "use_hierarchy",
* "expand_hierarchy",
* "enable_parent_when_child_gets_disabled",
* "exclude",
* "only_visible_when_facet_source_is_visible",
* "processor_configs",
......@@ -99,6 +102,20 @@ class Facet extends ConfigEntityBase implements FacetInterface {
*/
protected $widgetInstance;
/**
* The hierarchy definition.
*
* @var array
*/
protected $hierarchy;
/**
* The hierarchy instance.
*
* @var \Drupal\facets\Hierarchy\HierarchyPluginBase
*/
protected $hierarchy_processor;
/**
* The operator to hand over to the query, currently AND | OR.
*
......@@ -106,6 +123,27 @@ class Facet extends ConfigEntityBase implements FacetInterface {
*/
protected $query_operator;
/**
* A boolean indicating if items should be rendered in hierarchical structure.
*
* @var bool
*/
protected $use_hierarchy = FALSE;
/**
* A boolean indicating if hierarchical items should always be expanded.
*
* @var bool
*/
protected $expand_hierarchy = FALSE;
/**
* Wether or not parents should be enabled when a child gets disabled.
*
* @var bool
*/
protected $enable_parent_when_child_gets_disabled = TRUE;
/**
* A boolean flag indicating if search should exclude selected facets.
*
......@@ -232,6 +270,14 @@ class Facet extends ConfigEntityBase implements FacetInterface {
*/
protected $widget_plugin_manager;
/**
* The hierarchy plugin manager.
*
* @var \Drupal\facets\Hierarchy\HierarchyPluginManager
* The hierarchy plugin manager.
*/
protected $hierarchy_manager;
/**
* The facet source config object.
*
......@@ -260,6 +306,16 @@ class Facet extends ConfigEntityBase implements FacetInterface {
return $this->widget_plugin_manager ?: $container->get('plugin.manager.facets.widget');
}
/**
* Returns the hierarchy plugin manager.
*
* @return \Drupal\facets\Hierarchy\HierarchyPluginManager
* The hierarchy plugin manager.
*/
public function getHierarchyManager() {
return $this->hierarchy_manager ?: \Drupal::service('plugin.manager.facets.hierarchy');
}
/**
* {@inheritdoc}
*/
......@@ -318,6 +374,43 @@ class Facet extends ConfigEntityBase implements FacetInterface {
return $this->widgetInstance;
}
/**
* {@inheritdoc}
*/
public function setHierarchy($id, array $configuration = NULL) {
if ($configuration === NULL) {
$instance = $this->getHierarchyManager()->createInstance($id);
// Get the default configuration for this plugin.
$configuration = $instance->getConfiguration();
}
$this->hierarchy = ['type' => $id, 'config' => $configuration];
// Unset the hierarchy instance, if exists.
unset($this->hierarchy_instance);
}
/**
* {@inheritdoc}
*/
public function getHierarchy() {
// TODO: do not hardcode on taxonomy, make this configurable (or better,
// autoselected depending field type).
return ['type' => 'taxonomy', 'config' => []];
// return $this->hierarchy;
}
/**
* {@inheritdoc}
*/
public function getHierarchyInstance() {
if (!isset($this->hierarchy_instance)) {
$definition = $this->getHierarchy();
$this->hierarchy_instance = $this->getHierarchyManager()
->createInstance($definition['type'], (array) $definition['config']);
}
return $this->hierarchy_instance;
}
/**
* Retrieves all processors supported by this facet.
*
......@@ -389,6 +482,48 @@ class Facet extends ConfigEntityBase implements FacetInterface {
return $this->query_operator ?: 'or';
}
/**
* {@inheritdoc}
*/
public function setUseHierarchy($use_hierarchy) {
return $this->use_hierarchy = $use_hierarchy;
}
/**
* {@inheritdoc}
*/
public function getUseHierarchy() {
return $this->use_hierarchy;
}
/**
* {@inheritdoc}
*/
public function setExpandHierarchy($expand_hierarchy) {
return $this->expand_hierarchy = $expand_hierarchy;
}
/**
* {@inheritdoc}
*/
public function getExpandHierarchy() {
return $this->expand_hierarchy;
}
/**
* {@inheritdoc}
*/
public function setEnableParentWhenChildGetsDisabled($enable_parent_when_child_gets_disabled) {
return $this->enable_parent_when_child_gets_disabled = $enable_parent_when_child_gets_disabled;
}
/**
* {@inheritdoc}
*/
public function getEnableParentWhenChildGetsDisabled() {
return $this->enable_parent_when_child_gets_disabled;
}
/**
* {@inheritdoc}
*/
......
......@@ -40,6 +40,35 @@ interface FacetInterface extends ConfigEntityInterface {
*/
public function getWidgetInstance();
/**
* Sets the facet hierarchy definition.
*
* @param string $id
* The hierarchy plugin id.
* @param array $configuration
* (optional) The facet hierarchy plugin configuration. When empty, the
* default plugin configuration will be used.
*/
public function setHierarchy($id, array $configuration = NULL);
/**
* Returns the facet hierarchy definition.
*
* @return array
* An associative array with the following structure:
* - id: The hierarchy plugin id as a string.
* - config: The widget configuration as an array.
*/
public function getHierarchy();
/**
* Returns the facet hierarchy instance.
*
* @return \Drupal\facets\Hierarchy\HierarchyPluginBase
* The plugin instance
*/
public function getHierarchyInstance();
/**
* Returns field identifier.
*
......@@ -180,6 +209,63 @@ interface FacetInterface extends ConfigEntityInterface {
*/
public function getExclude();
/**
* Returns the value of the use_hierarchy boolean.
*
* This will return true when the results in the facet should be rendered in
* a hierarchical structure.
*
* @return bool
* A boolean flag indicating if results should be rendered using hierarchy.
*/
public function getUseHierarchy();
/**
* Sets the use_hierarchy.
*
* @param bool $use_hierarchy
* A boolean flag indicating if results should be rendered using hierarchy.
*/
public function setUseHierarchy($use_hierarchy);
/**
* Returns the value of the expand_hierarchy boolean.
*
* This will return true when the results in the facet should be expanded in
* a hierarchical structure, regardless of active state.
*
* @return bool
* Wether or not results should always be expanded using hierarchy.
*/
public function getExpandHierarchy();
/**
* Sets the expand_hierarchy.
*
* @param bool $expand_hierarchy
* Wether or not results should always be expanded using hierarchy.
*/
public function setExpandHierarchy($expand_hierarchy);
/**
* Returns the value of the enable_parent_when_child_gets_disabled boolean.
*
* This will return true when the parent item in the facet should be enabled
* in an hierarchical structure, when a child facet item gets disabled.
*
* @return bool
* Wether or not parents should be enabled when a child gets disabled.
*/
public function getEnableParentWhenChildGetsDisabled();
/**
* Sets the enable_parent_when_child_gets_disabled.
*
* @param bool $enable_parent_when_child_gets_disabled
* Wether or not parents should be enabled when a child gets disabled.
*/
public function setEnableParentWhenChildGetsDisabled($enable_parent_when_child_gets_disabled);
/**
* Returns the plugin name for the url processor.
*
......
......@@ -65,6 +65,11 @@ class DefaultFacetManager {
*/
protected $facets = [];
/**
* An array of all entity ids in the active resultset which are a child.
*/
protected $childIds = [];
/**
* An array flagging which facet source' facets have been processed.
*
......@@ -292,6 +297,24 @@ class DefaultFacetManager {
$results = $processor->build($facet, $results);
}
// Handle hierarchy.
if ($results && $facet->getUseHierarchy()) {
$keyed_results = [];
foreach ($results as $result) {
$keyed_results[$result->getRawValue()] = $result;
}
$parent_groups = $facet->getHierarchyInstance()->getChildIds(array_keys($keyed_results));
$keyed_results = $this->buildHierarchicalTree($keyed_results, $parent_groups);
// Remove children from primary level.
foreach (array_unique($this->childIds) as $child_id) {
unset($keyed_results[$child_id]);
}
$results = array_values($keyed_results);
}
// Trigger sort stage.
$active_sort_processors = [];
foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_SORT) as $processor) {
......@@ -377,4 +400,37 @@ class DefaultFacetManager {
return !empty($this->facets[$facet->id()]) ? $this->facets[$facet->id()] : NULL;
}
/**
* Builds an hierarchical structure for results.
*
* When given an array of results and an array which defines the hierarchical
* structure, this will build the results structure and set all childs.
*
* @param \Drupal\facets\Result\ResultInterface[] $keyed_results
* An array of results keyed by id.
* @param array $parent_groups
* An array of 'child id arrays' keyed by their parent id.
*
* @return \Drupal\facets\Result\ResultInterface[]
* An array of results structured hierarchicaly.
*/
protected function buildHierarchicalTree($keyed_results, $parent_groups) {
foreach ($keyed_results as &$result) {
$current_id = $result->getRawValue();
if (isset($parent_groups[$current_id]) && $parent_groups[$current_id]) {
$child_ids = $parent_groups[$current_id];
$child_keyed_results = [];
foreach($child_ids as $child_id){
if(isset($keyed_results[$child_id])){
$child_keyed_results[$child_id] = $keyed_results[$child_id];
}
}
$result->setChildren($this->buildHierarchicalTree($child_keyed_results, $parent_groups));
$this->childIds = array_merge($this->childIds, $child_ids);
}
}
return $keyed_results;
}
}
......@@ -371,6 +371,37 @@ class FacetForm extends EntityForm {
'#default_value' => $facet->getExclude(),
];
$form['facet_settings']['use_hierarchy'] = [
'#type' => 'checkbox',
'#title' => $this->t('Use hierarchy'),
'#description' => $this->t('Renders the items using hierarchy. Requires the hierarchy processor configured in search api for this field. If disabled all items will be flatten.') . '<br/><strong>At this moment only hierarchical taxonomy terms are supported.</strong>',
'#default_value' => $facet->getUseHierarchy(),
];
$form['facet_settings']['expand_hierarchy'] = [
'#type' => 'checkbox',
'#title' => $this->t('Always expand hierarchy'),
'#description' => $this->t('Render entire tree, regardless of whether the parents are active or not.'),
'#default_value' => $facet->getExpandHierarchy(),
'#states' => array(
'visible' => array(
':input[name="facet_settings[use_hierarchy]"]' => array('checked' => TRUE),
),
),
];
$form['facet_settings']['enable_parent_when_child_gets_disabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable parent when child gets disabled'),
'#description' => $this->t('Uncheck this if you want to allow de-activating an entire hierarchical trail by clicking an active child.'),
'#default_value' => $facet->getEnableParentWhenChildGetsDisabled(),
'#states' => array(
'visible' => array(
':input[name="facet_settings[use_hierarchy]"]' => array('checked' => TRUE),
),
),
];
$form['facet_settings']['weight'] = [
'#type' => 'number',
'#title' => $this->t('Weight'),
......@@ -565,6 +596,9 @@ class FacetForm extends EntityForm {
$facet->setQueryOperator($form_state->getValue(['facet_settings', 'query_operator']));
$facet->setExclude($form_state->getValue(['facet_settings', 'exclude']));
$facet->setUseHierarchy($form_state->getValue(['facet_settings', 'use_hierarchy']));
$facet->setExpandHierarchy($form_state->getValue(['facet_settings', 'expand_hierarchy']));
$facet->setEnableParentWhenChildGetsDisabled($form_state->getValue(['facet_settings', 'enable_parent_when_child_gets_disabled']));
$facet->save();
drupal_set_message(t('Facet %name has been updated.', ['%name' => $facet->getName()]));
......
<?php
namespace Drupal\facets\Hierarchy;
/**
* Interface HierarchyInterface.
*/
interface HierarchyInterface {
/**
* Retrieve all parent ids for one specific id.
*
* @param string $id
* An entity id.
*
* @return array
* An array of all parent ids.
*/
public function getParentIds($id);
/**
* Retrieve all children and nested children for one specific id.
*
* @param string $id
* An entity id.
*
* @return array
* An array of all child ids.
*/
public function getNestedChildIds($id);
/**
* Retrieve the direct children for an array of ids.
*
* @param array $ids
* An array of ids.
*
* @return array
* Given parent ids as key, value is an array of child ids.
*/
public function getChildIds($ids);
}
<?php
namespace Drupal\facets\Hierarchy;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A base class for plugins that implements most of the boilerplate.
*/
abstract class HierarchyPluginBase extends ProcessorPluginBase implements HierarchyInterface, ContainerFactoryPluginInterface {
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
/** @var \Symfony\Component\HttpFoundation\Request $request */
$request = $container->get('request_stack')->getMasterRequest();
return new static($configuration, $plugin_id, $plugin_definition, $request);
}
}
<?php
namespace Drupal\facets\Hierarchy;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Manages Hierarchy plugins.
*
* @see \Drupal\facets\Annotation\FacetsHierarchy
* @see \Drupal\facets\Hierarchy\HierarchyInterface
* @see plugin_api
*/
class HierarchyPluginManager extends DefaultPluginManager {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/facets/hierarchy', $namespaces, $module_handler, 'Drupal\facets\Hierarchy\HierarchyInterface', 'Drupal\facets\Annotation\FacetsHierarchy');
$this->setCacheBackend($cache_backend, 'facets_hierarchy');
}
}
<?php
namespace Drupal\facets\Plugin\facets\hierarchy;
use Drupal\facets\Hierarchy\HierarchyPluginBase;
/**
* Taxonomy hierarchy.
*
* @FacetsHierarchy(
* id = "taxonomy",
* label = @Translation("Taxonomy hierarchy"),
* description = @Translation("Hierarchy structure provided by the taxonomy module.")
* )
*/
class Taxonomy extends HierarchyPluginBase {
/**
* {@inheritdoc}
*/
public function getParentIds($id) {
$current_tid = $id;
while ($parent = $this->taxonomyGetParent($current_tid)) {
$current_tid = $parent;
$parents[$id][] = $parent;
}
return isset($parents[$id]) ? $parents[$id] : [];
}
/**
* {@inheritdoc}
*/
public function getNestedChildIds($id) {
$children = &drupal_static(__FUNCTION__, []);
if (!isset($children[$id])) {
// TODO: refactor to swap out deprecated db_select.
$query = db_select('taxonomy_term_hierarchy', 'h');
$query->addField('h', 'tid');
$query->condition('h.parent', $id);
$queried_children = $query->execute()->fetchCol();
$subchilds = [];
foreach ($queried_children as $child) {
$subchilds = array_merge($subchilds, $this->getNestedChildIds($child));
}
$children[$id] = array_merge($queried_children, $subchilds);
}
return isset($children[$id]) ? $children[$id] : [];
}
/**
* {@inheritdoc}
*/
public function getChildIds($ids) {
// TODO: refactor to swap out deprecated db_select.
// TODO: also check if this query does not too much, plain d7 c/p here.
$result = db_select('taxonomy_term_hierarchy', 'th')
->fields('th', array('tid', 'parent'))
->condition('th.parent', '0', '>')
->condition(db_or()
->condition('th.tid', $ids, 'IN')
->condition('th.parent', $ids, 'IN')
)
->execute();
$parents = array();
foreach ($result as $record) {
$parents[$record->parent][] = $record->tid;
}
return $parents;
}
/**
* Returns the parent tid for a given tid, or false if no parent exists.
*
* @param int $tid
* A taxonomy term id.
*
* @return int|false
* Returns FALSE if no parent is found, else parent tid.
*/
protected function taxonomyGetParent($tid) {
// TODO: refactor to swap out deprecated db_select.
$parent = &drupal_static(__FUNCTION__, []);
if (!isset($parent[$tid])) {
$query = db_select('taxonomy_term_hierarchy', 'h');
$query->addField('h', 'parent');
$query->condition('h.tid', $tid);
$parent[$tid] = $query->execute()->fetchField();
}
return isset($parent[$tid]) ? $parent[$tid] : FALSE;
}
}
......@@ -72,11 +72,11 @@ class QueryString extends UrlProcessorPluginBase {
if ($facet->getFacetSource()->getPath()) {
$request = Request::create($facet->getFacetSource()->getPath());
}
$url = Url::createFromRequest($request);
$url->setOption('attributes', ['rel' => 'nofollow']);