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