diff --git a/config/schema/facets.facet.schema.yml b/config/schema/facets.facet.schema.yml
index 4bffcaa..6691324 100644
--- a/config/schema/facets.facet.schema.yml
+++ b/config/schema/facets.facet.schema.yml
@@ -32,6 +32,9 @@ facets.facet.*:
exclude:
type: boolean
label: 'Exclude'
+ use_hierarchy:
+ type: boolean
+ label: 'Use hierarchy'
widget:
type: mapping
label: 'Facet widget'
diff --git a/src/Entity/Facet.php b/src/Entity/Facet.php
index 848713d..4b0fe8e 100644
--- a/src/Entity/Facet.php
+++ b/src/Entity/Facet.php
@@ -41,6 +41,8 @@ use Drupal\facets\FacetInterface;
* "facet_source_id",
* "widget",
* "query_operator",
+ * "use_hierarchy",
+ * "expand_hierarchy",
* "exclude",
* "only_visible_when_facet_source_is_visible",
* "processor_configs",
@@ -107,6 +109,20 @@ 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;
+
+ /**
+ * A boolean indicating if hierarchical items should always be expanded.
+ *
+ * @var bool
+ */
+ protected $expand_hierarchy;
+
+ /**
* A boolean flag indicating if search should exclude selected facets.
*
* @var bool
@@ -392,6 +408,34 @@ class Facet extends ConfigEntityBase implements FacetInterface {
/**
* {@inheritdoc}
*/
+ public function setUseHierarchy($use_hierarchy) {
+ return $this->use_hierarchy = $use_hierarchy;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUseHierarchy() {
+ return isset($this->use_hierarchy) ? $this->use_hierarchy : false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setExpandHierarchy($expand_hierarchy) {
+ return $this->expand_hierarchy = $expand_hierarchy;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExpandHierarchy() {
+ return isset($this->expand_hierarchy) ? $this->expand_hierarchy : false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function setExclude($exclude) {
return $this->exclude = $exclude;
}
diff --git a/src/FacetInterface.php b/src/FacetInterface.php
index 5e9180d..9d2c48f 100644
--- a/src/FacetInterface.php
+++ b/src/FacetInterface.php
@@ -181,6 +181,44 @@ 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 plugin name for the url processor.
*
* @return string
diff --git a/src/FacetManager/DefaultFacetManager.php b/src/FacetManager/DefaultFacetManager.php
index c53e541..a927c35 100644
--- a/src/FacetManager/DefaultFacetManager.php
+++ b/src/FacetManager/DefaultFacetManager.php
@@ -65,6 +65,8 @@ class DefaultFacetManager {
*/
protected $facets = [];
+ protected $child_tids = [];
+
/**
* An array flagging which facet source' facets have been processed.
*
@@ -292,6 +294,26 @@ class DefaultFacetManager {
$results = $processor->build($facet, $results);
}
+ // Handle hierarchy.
+ if($facet->getUseHierarchy()){
+ $keyed_results = [];
+ foreach ($results as $result) {
+ $keyed_results[$result->getRawValue()] = $result;
+ }
+
+ $parent_groups = $this->getTaxonomyHierarchy(array_keys($keyed_results));
+ // Fire the recursive buildTree.
+ $keyed_results = $this->buildTree($keyed_results, $parent_groups);
+
+ // Remove children from primary level.
+ foreach (array_unique($this->child_tids) as $child_tid) {
+ unset($keyed_results[$child_tid]);
+ }
+
+ $results = array_values($keyed_results);
+
+ }
+
// Trigger sort stage.
$active_sort_processors = [];
foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_SORT) as $processor) {
@@ -366,4 +388,46 @@ class DefaultFacetManager {
return !empty($this->facets[$facet->id()]) ? $this->facets[$facet->id()] : NULL;
}
+ protected function buildTree($keyed_results, $parent_groups) {
+
+ foreach ($keyed_results as $tid => &$result) {
+ $current_tid = $result->getRawValue();
+ $child_tids = array_keys($parent_groups, $current_tid);
+ if ($child_tids) {
+ $child_keyed_results = array_filter($keyed_results, function ($k) use ($child_tids) {
+ return in_array($k, $child_tids);
+ }, ARRAY_FILTER_USE_KEY);
+ $result->setChildren($this->buildTree($child_keyed_results, $parent_groups));
+ $this->child_tids = array_merge($this->child_tids, $child_tids);
+ }
+ }
+
+ return $keyed_results;
+ }
+
+ /**
+ * Gets parent information for taxonomy terms.
+ *
+ * @param array $values
+ * An array containing the term ids.
+ *
+ * @return
+ * An associative array keyed by term ID to parent ID.
+ */
+ protected function getTaxonomyHierarchy(array $values) {
+ $result = db_select('taxonomy_term_hierarchy', 'th')
+ ->fields('th', array('tid', 'parent'))
+ ->condition('th.parent', '0', '>')
+ ->condition(db_or()
+ ->condition('th.tid', $values, 'IN')
+ ->condition('th.parent', $values, 'IN')
+ )
+ ->execute();
+
+ $parents = array();
+ foreach ($result as $record) {
+ $parents[$record->tid] = $record->parent;
+ }
+ return $parents;
+ }
}
diff --git a/src/Form/FacetForm.php b/src/Form/FacetForm.php
index fd8b01f..eb5e4f2 100644
--- a/src/Form/FacetForm.php
+++ b/src/Form/FacetForm.php
@@ -380,6 +380,25 @@ 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.') . '
BETA: at this moment only hierarchical taxonomy terms are supported.',
+ '#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']['weight'] = [
'#type' => 'number',
'#title' => $this->t('Weight'),
@@ -581,6 +600,8 @@ 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->save();
drupal_set_message(t('Facet %name has been updated.', ['%name' => $facet->getName()]));
diff --git a/src/Plugin/facets/query_type/SearchApiString.php b/src/Plugin/facets/query_type/SearchApiString.php
index 03504e6..c5e7022 100644
--- a/src/Plugin/facets/query_type/SearchApiString.php
+++ b/src/Plugin/facets/query_type/SearchApiString.php
@@ -54,6 +54,34 @@ class SearchApiString extends QueryTypePluginBase {
// Add the filter to the query if there are active values.
$active_items = $this->facet->getActiveItems();
+ // When the operator is OR, remove parents from the active ones if
+ // children are active. If we don't do this, sending a child and its
+ // parent will produce the same results as just sending the parent.
+ if($active_items && $this->facet->getUseHierarchy() && $this->facet->getQueryOperator() == 'or'){
+
+ // TODO: make this callback configurable, dont hardcode on terms.
+ $parents = $this->getTaxonomyHierarchy($active_items);
+
+ // Check the filters in reverse order, to avoid checking parents that
+ // will afterwards be removed anyways.
+ foreach (array_reverse($active_items) as $filter) {
+ // Skip this filter if it was already removed, or if it is the
+ // "missing value" filter ("!").
+ if (!in_array($filter, $active_items) || !is_numeric($filter)) {
+ continue;
+ }
+ // Go through the entire hierarchy of the value and remove all its
+ // ancestors.
+ while(!empty($parents[$filter])){
+ $active_items = array_diff( $active_items, [$parents[$filter]] ) ;
+ unset($parents[$filter]);
+ if(isset($parents[$parents[$filter]])){
+ $filter = $parents[$parents[$filter]];
+ }
+ }
+ }
+ }
+
if (count($active_items)) {
$filter = $query->createConditionGroup($operator, ['facet:' . $field_identifier]);
foreach ($active_items as $value) {
@@ -86,4 +114,51 @@ class SearchApiString extends QueryTypePluginBase {
return $this->facet;
}
+
+ /**
+ * Gets parent information for taxonomy terms.
+ *
+ * @param array $values
+ * An array containing the term ids.
+ *
+ * @return
+ * An associative array keyed by term ID to parent ID.
+ */
+ function facetapi_get_taxonomy_hierarchy(array $values) {
+ $result = db_select('taxonomy_term_hierarchy', 'th')
+ ->fields('th', array('tid', 'parent'))
+ ->condition('th.parent', '0', '>')
+ ->condition(db_or()
+ ->condition('th.tid', $values, 'IN')
+ ->condition('th.parent', $values, 'IN')
+ )
+ ->execute();
+
+ $parents = array();
+ foreach ($result as $record) {
+ $parents[$record->tid][] = $record->parent;
+ }
+ return $parents;
+ }
+
+ /**
+ * Temporary hardcoded function, needs to move to a HierarchyPlugin once it exists.
+ */
+ protected function getTaxonomyHierarchy(array $values) {
+ $result = db_select('taxonomy_term_hierarchy', 'th')
+ ->fields('th', array('tid', 'parent'))
+ ->condition('th.parent', '0', '>')
+ ->condition(db_or()
+ ->condition('th.tid', $values, 'IN')
+ ->condition('th.parent', $values, 'IN')
+ )
+ ->execute();
+
+ $parents = array();
+ foreach ($result as $record) {
+ $parents[$record->tid] = $record->parent;
+ }
+ return $parents;
+ }
+
}
diff --git a/src/Result/Result.php b/src/Result/Result.php
index 6842121..555b761 100644
--- a/src/Result/Result.php
+++ b/src/Result/Result.php
@@ -129,8 +129,8 @@ class Result implements ResultInterface {
/**
* {@inheritdoc}
*/
- public function setChildren(ResultInterface $children) {
- $this->children[] = $children;
+ public function setChildren(array $children) {
+ $this->children = $children;
}
/**
diff --git a/src/Result/ResultInterface.php b/src/Result/ResultInterface.php
index d721777..0dba2ee 100644
--- a/src/Result/ResultInterface.php
+++ b/src/Result/ResultInterface.php
@@ -80,7 +80,7 @@ interface ResultInterface {
* @param \Drupal\facets\Result\ResultInterface $children
* The children to be added.
*/
- public function setChildren(ResultInterface $children);
+ public function setChildren(array $children);
/**
* Returns children results.
diff --git a/src/Widget/WidgetPluginBase.php b/src/Widget/WidgetPluginBase.php
index 324da68..2ade651 100644
--- a/src/Widget/WidgetPluginBase.php
+++ b/src/Widget/WidgetPluginBase.php
@@ -132,16 +132,19 @@ abstract class WidgetPluginBase extends PluginBase implements WidgetPluginInterf
*/
protected function buildListItems(ResultInterface $result) {
$classes = ['facet-item'];
- if ($children = $result->getChildren()) {
+ $children = $result->getChildren();
+ if ($children && ($result->isActive() || $this->facet->getExpandHierarchy())) {
$items = $this->prepareLink($result);
- $children_markup = [];
+ $child_items = [];
foreach ($children as $child) {
- $children_markup[] = $this->buildChild($child);
+ $child_items[] = $this->buildListItems($child);
}
$classes[] = 'expanded';
- $items['children'] = [$children_markup];
+ $items['children'] = [
+ '#theme' => 'item_list',
+ '#items' => $child_items];
if ($result->isActive()) {
$items['#attributes'] = ['class' => 'active-trail'];
@@ -157,7 +160,6 @@ abstract class WidgetPluginBase extends PluginBase implements WidgetPluginInterf
$items['#wrapper_attributes'] = ['class' => $classes];
$items['#attributes']['data-drupal-facet-item-id'] = $this->facet->getUrlAlias() . '-' . $result->getRawValue();
-
return $items;
}
@@ -181,21 +183,6 @@ abstract class WidgetPluginBase extends PluginBase implements WidgetPluginInterf
}
/**
- * Builds a result item as a render array.
- *
- * @param \Drupal\facets\Result\ResultInterface $child
- * A result item.
- *
- * @return array
- * The result item as render array.
- */
- protected function buildChild(ResultInterface $child) {
- $item = $this->prepareLink($child);
- $item['#wrapper_attributes'] = ['class' => ['leaf']];
- return $item;
- }
-
- /**
* Builds a facet result item.
*
* @param \Drupal\facets\Result\ResultInterface $result