' . t('About') . ''; $output .= '
' . t('Facets test') . '
'; return $output; case 'entity.facets_facet.collection': $output = ''; $output .= '' . t('Below is a list of facets grouped by facetsources they are associated with. A facetsource is the instance where the facet does the actual filtering, for example a View on a Search API index.') . '
'; $output .= '' . t('The facets weight can be changed with drag and drop within the same facet source. Although you can drag and drop a facet under any facet source, this change will not be performed on save.') . '
'; return $output; } } /** * Implements hook_theme(). */ function facets_theme($existing, $type, $theme, $path) { return [ 'facets_result_item' => [ 'base hook' => 'facets_result_item', 'variables' => [ 'facet' => NULL, 'raw_value' => '', 'value' => '', 'show_count' => FALSE, 'count' => NULL, 'is_active' => FALSE, ], ], 'facets_item_list' => [ 'variables' => [ 'facet' => NULL, 'items' => [], 'title' => '', 'list_type' => 'ul', 'wrapper_attributes' => [], 'attributes' => [], 'empty' => NULL, 'context' => [], ], ], ]; } /** * Implements hook_search_api_query_alter(). */ function facets_search_api_query_alter(QueryInterface &$query) { if ($query->getIndex()->getServerInstance()->supportsFeature('search_api_facets')) { /** @var \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager */ $facet_manager = \Drupal::service('facets.manager'); // It's safe to hardcode this to the search api scheme because this is in a // search_api_query_alter method. If this generated source is not correct, // implementing the same alter and directly calling // $manager->alterQuery($query, $your_facetsource_id); will fix that. $facet_source = 'search_api:' . str_replace(':', '__', $query->getSearchId()); // Add the active filters. $facet_manager->alterQuery($query, $facet_source); } } /** * Implements hook_entity_presave(). * * We implement this to make sure that a facet gets removed on view updates, so * we don't get broken facet blocks. */ function facets_entity_presave(EntityInterface $entity) { // Make sure that we only react on view entities with changed displays. if ($entity instanceof View && !empty($entity->original)) { if ($entity->original->get('display') != $entity->get('display')) { /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_plugin_manager */ $facet_source_plugin_manager = \Drupal::getContainer() ->get('plugin.manager.facets.facet_source'); $definitions = $facet_source_plugin_manager->getDefinitions(); // Setup an array of sources that are deleted. $sources = []; foreach ($entity->original->get('display') as $k => $display) { // Check if the current display is also a facet source plugin and that // is removed from the view. We use the double underscore here to make // sure that we use core convention of "plugin:derived_plugin". $facets_source_plugin_id = 'search_api:views_' . $display['display_plugin'] . '__' . $entity->id() . '__' . $display['id']; if (array_key_exists($facets_source_plugin_id, $definitions) && !array_key_exists($k, $entity->get('display'))) { $entity_id = str_replace(':', '__', $facets_source_plugin_id); $source_entity = FacetSource::load($entity_id); $sources[] = $facets_source_plugin_id; if (!is_null($source_entity)) { $source_entity->delete(); } } } // Loop over all deleted sources and delete the facets that were linked to // that source. if (count($sources) > 0) { /** @var \Drupal\facets\FacetManager\DefaultFacetManager $fm */ $fm = \Drupal::getContainer()->get('facets.manager'); foreach ($sources as $source) { $facets = $fm->getFacetsByFacetSourceId($source); foreach ($facets as $facet) { $facet->delete(); } } } $facet_source_plugin_manager->clearCachedDefinitions(); } } } /** * Implements hook_preprocess_block(). * * Adds a class for the widget to the facet block to allow for more specific * styling. */ function facets_preprocess_block(&$variables) { if ($variables['configuration']['provider'] == 'facets') { if (!empty($variables['derivative_plugin_id'])) { $facet = Facet::load($variables['derivative_plugin_id']); $variables['attributes']['class'][] = 'block-facet--' . Html::cleanCssIdentifier($facet->getWidget()['type']); } } } /** * Implements hook_entity_predelete(). * * We implement this hook to make sure that facet source plugins are cleared * when a view is deleted. It also deletes facets that are created on those * plugins. */ function facets_entity_predelete(EntityInterface $entity) { if ($entity instanceof View) { $facet_source_plugin_manager = \Drupal::getContainer() ->get('plugin.manager.facets.facet_source'); $definitions = $facet_source_plugin_manager->getDefinitions(); if (!is_array($definitions)) { return; } foreach ($definitions as $plugin_id => $definition) { if (strpos($plugin_id, 'search_api:' . $entity->id() . '__') !== FALSE) { try { $facetManager = \Drupal::getContainer()->get('facets.manager'); } catch (ServiceNotFoundException $e) { \Drupal::logger('facets')->log(RfcLogLevel::DEBUG, 'Facet manager not found on trying to delete a view.'); return; } $facets = $facetManager->getFacetsByFacetSourceId($plugin_id); foreach ($facets as $facet) { $facet->delete(); } } } // Clear cached plugin definitions for facet source to make sure we don't // show stale data. $facet_source_plugin_manager->clearCachedDefinitions(); } } /** * Prepares variables for facets item list templates. * * Default template: facets-item-list.html.twig. * * @param array $variables * An associative array containing: * - items: An array of items to be displayed in the list. Each item can be * either a string or a render array. If #type, #theme, or #markup * properties are not specified for child render arrays, they will be * inherited from the parent list, allowing callers to specify larger * nested lists without having to explicitly specify and repeat the * render properties for all nested child lists. * - title: A title to be prepended to the list. * - list_type: The type of list to return (e.g. "ul", "ol"). * - wrapper_attributes: HTML attributes to be applied to the list wrapper. * * @see https://www.drupal.org/node/1842756 */ function facets_preprocess_facets_item_list(array &$variables) { template_preprocess_item_list($variables); } /** * Implements hook_system_breadcrumb_alter(). */ function facets_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) { /** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_manager */ $facet_source_manager = \Drupal::service('plugin.manager.facets.facet_source'); /** @var \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager */ $facet_manager = \Drupal::service('facets.manager'); /** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */ $entity_type_manager = \Drupal::service('entity_type.manager'); /** @var \Drupal\Core\Entity\EntityStorageInterface $facet_source_storage */ $facet_source_storage = $entity_type_manager->getStorage('facets_facet_source'); $facet_sources_definitions = $facet_source_manager->getDefinitions(); // No facet sources found, so don't do anything. if (empty($facet_sources_definitions)) { return; } foreach ($facet_sources_definitions as $definition) { /* @var \Drupal\facets\FacetSource\FacetSourcePluginBase $facet_source_plugin */ $facet_source_plugin = $facet_source_manager->createInstance($definition['id']); // If the current facet source is not being rendered, don't do anything with // these facet sources. if (!$facet_source_plugin->isRenderedInCurrentRequest()) { continue; } $source_id = str_replace(':', '__', $definition['id']); /** @var \Drupal\facets\FacetSourceInterface $facet_source */ $facet_source = $facet_source_storage->load($source_id); // If the facet source is not loaded, or the facet source doesn't have // breadcrumbs enabled, don't do anything. if (!($facet_source && !empty($facet_source->getBreadcrumbSettings()['active']))) { continue; } // Add the required cacheability metadata. $breadcrumb->addCacheContexts(['url']); $breadcrumb->addCacheableDependency($facet_source); // Process the facets if they are not already processed. $facet_manager->processFacets(); $facets = $facet_manager->getFacetsByFacetSourceId($definition['id']); // Sort facets by weight. uasort($facets, function (FacetInterface $a, FacetInterface $b) { if ($a->getWeight() == $b->getWeight()) { return 0; } return ($a->getWeight() < $b->getWeight()) ? -1 : 1; }); /** @var \Drupal\facets\UrlProcessor\UrlProcessorPluginManager $url_processor_manager */ $url_processor_manager = \Drupal::service('plugin.manager.facets.url_processor'); // Get active facets and results to use them at building the crumbs. $active_results = []; $active_facets = []; foreach ($facets as $facet) { if (count($facet->getActiveItems()) > 0) { // Add the facet as a cacheable dependency. $breadcrumb->addCacheableDependency($facet); /** @var \Drupal\facets\UrlProcessor\UrlProcessorInterface $url_processor */ $url_processor = $url_processor_manager->createInstance($facet_source->getUrlProcessorName(), ['facet' => $facet]); $facet_manager->build($facet); foreach ($facet->getResults() as $result) { if ($result->isActive()) { // Clone the result so we can mark it as inactive to be added to the // url parameters when calling buildUrls. $cloned_result = clone $result; $cloned_result->setActiveState(FALSE); $active_results[$facet->getUrlAlias()][] = $cloned_result; } } if (!empty($active_results[$facet->getUrlAlias()])) { $url_processor->buildUrls($facet, $active_results[$facet->getUrlAlias()]); } $active_facets[$facet->getUrlAlias()] = $facet; } } // TODO find a better way to construct the url for a crumb maybe url // processor will have a function to get params for a result // without all the other request parameters; with this we could implement: // @see https://www.drupal.org/node/2861586 // TODO handle not grouped facets. /** @var \Drupal\facets\Result\ResultInterface[] $facet_results */ foreach ($active_results as $facet_alias => $facet_results) { $facet_used_result[] = $facet_alias; $facet_crumb_items = []; reset($facet_results); $facet_url = clone current($facet_results)->getUrl(); // Because we can't get the desired url trough a url processor method // we iterate each result url and remove the facet params that haven't // been used on previous crumbs. foreach ($facet_results as $res) { $facet_url = $res->getUrl(); /** @var \Drupal\Core\Url $facet_url */ $query = $facet_url->getOption('query'); $filter_key = $facet_source->getFilterKey() ?: 'f'; $source_filter = $query[$filter_key]; $source_filter = array_unique($source_filter); $source_filter = array_filter($source_filter); $matches = preg_grep('/^' . implode('|^', $facet_used_result) . '/', $source_filter); $query[$filter_key] = $matches; $facet_crumb_items[] = $res->getDisplayValue(); } sort($facet_crumb_items); // Set the new query and create the link. $facet_url->setOption('query', $query); $crumb_text = $active_facets[$facet_alias]->label() . ': ' . implode(', ', $facet_crumb_items); $link = Link::fromTextAndUrl($crumb_text, $facet_url); $breadcrumb->addLink($link); } } }