localgov_directories-3.3.1/src/DirectoryExtraFieldDisplay.php

src/DirectoryExtraFieldDisplay.php
<?php

namespace Drupal\localgov_directories;

use Drupal\Component\Utility\Html;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\localgov_directories\Constants as Directory;
use Drupal\localgov_directories\Entity\LocalgovDirectoriesFacets;
use Drupal\localgov_directories\Entity\LocalgovDirectoriesFacetsType;
use Drupal\node\NodeInterface;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Adds views display for the directory channel.
 */
class DirectoryExtraFieldDisplay implements ContainerInjectionInterface, TrustedCallbackInterface {

  use StringTranslationTrait;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The entity repository.
   *
   * @var \Drupal\Core\Entity\EntityRepositoryInterface
   */
  protected $entityRepository;

  /**
   * Entity field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;

  /**
   * The block plugin manager.
   *
   * @var \Drupal\Core\Block\BlockManagerInterface
   */
  protected $pluginBlockManager;

  /**
   * Form builder.
   *
   * @var \Drupal\Core\Form\FormBuilderInterface
   */
  protected $formBuilder;

  /**
   * Current route match.
   *
   * @var \Drupal\Core\Routing\RouteMatchInterface
   */
  protected $routeMatch;

  /**
   * DirectoryExtraFieldDisplay constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   Entity type manager.
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
   *   Entity repository.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   Entity Field Manager.
   * @param \Drupal\Core\Block\BlockManagerInterface $plugin_manager_block
   *   Plugin Block Manager.
   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
   *   Form Builder.
   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
   *   Current route match.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, EntityFieldManagerInterface $entity_field_manager, BlockManagerInterface $plugin_manager_block, FormBuilderInterface $form_builder, RouteMatchInterface $route_match) {
    $this->entityTypeManager = $entity_type_manager;
    $this->entityRepository = $entity_repository;
    $this->entityFieldManager = $entity_field_manager;
    $this->pluginBlockManager = $plugin_manager_block;
    $this->formBuilder = $form_builder;
    $this->routeMatch = $route_match;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('entity.repository'),
      $container->get('entity_field.manager'),
      $container->get('plugin.manager.block'),
      $container->get('form_builder'),
      $container->get('current_route_match')
    );
  }

  /**
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return [
      'removeExposedFilter',
    ];
  }

  /**
   * Gets the "extra fields" for a bundle.
   *
   * @see hook_entity_extra_field_info()
   */
  public function entityExtraFieldInfo() {
    $fields = [];
    $fields['node']['localgov_directory']['display']['localgov_directory_view'] = [
      'label' => $this->t('Directory listing'),
      'description' => $this->t("Output from the embedded view for this channel."),
      'weight' => -20,
      'visible' => TRUE,
    ];
    $fields['node']['localgov_directory']['display']['localgov_directory_view_with_search'] = [
      'label' => $this->t('Directory listing (with search box)'),
      'description' => $this->t("Output from the embedded view for this channel. With search exposed filter. Use if not including the search block."),
      'weight' => -20,
      'visible' => FALSE,
    ];
    $fields['node']['localgov_directory']['display']['localgov_directory_facets'] = [
      'label' => $this->t('Directory facets'),
      'description' => $this->t("Output facets block, field alternative to enabling the block."),
      'weight' => -20,
      'visible' => TRUE,
    ];

    foreach ($this->directoryEntryTypes() as $type_id) {
      $fields['node'][$type_id]['display']['localgov_directory_search'] = [
        'label' => $this->t('Directory search'),
        'description' => $this->t("Free text search field for directories the entry is in."),
        'weight' => -20,
        'visible' => TRUE,
      ];
    }

    return $fields;
  }

  /**
   * Get all node bundles that are directory entry types.
   *
   * @return string[]
   *   Bundle IDs.
   */
  public function directoryEntryTypes() {
    $entry_types = [];

    $node_types = $this->entityTypeManager->getStorage('node_type')->loadMultiple();
    foreach ($node_types as $type_id => $type) {
      $fields = $this->entityFieldManager->getFieldDefinitions('node', $type_id);
      if (isset($fields[Directory::CHANNEL_SELECTION_FIELD])) {
        $entry_types[$type_id] = $type_id;
      }
    }

    return $entry_types;
  }

  /**
   * Adds view with arguments to view render array if required.
   *
   * @see localgov_directories_node_view()
   */
  public function nodeView(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, $view_mode) {
    if ($display->getComponent('localgov_directory_view')) {
      $build['localgov_directory_view'] = $this->getViewEmbed($node);
    }
    if ($display->getComponent('localgov_directory_view_with_search')) {
      $build['localgov_directory_view'] = $this->getViewEmbed($node, TRUE);
    }
    if ($display->getComponent('localgov_directory_facets')) {
      $build['localgov_directory_facets'] = $this->getFacetsBlock($node);
    }
    if ($display->getComponent('localgov_directory_search')) {
      $build['localgov_directory_search'] = $this->getSearchBlock($node);
    }
  }

  /**
   * Retrieves view, and sets render array.
   */
  protected function getViewEmbed(NodeInterface $node, $search_filter = FALSE) {
    $view = Views::getView(Directory::CHANNEL_VIEW);
    $views_display = self::determineChannelViewDisplay($node);
    if (!$view || !$view->access($views_display)) {
      return;
    }
    $render = [
      '#type' => 'view',
      '#name' => Directory::CHANNEL_VIEW,
      '#display_id' => $views_display,
      '#arguments' => [$node->id()],
    ];
    if (!$search_filter) {
      $render['#post_render'] = [
        [static::class, 'removeExposedFilter'],
      ];
    }
    return $render;
  }

  /**
   * Retrieves the facets block for a directory.
   */
  protected function getFacetsBlock(NodeInterface $node) {
    // The facet manager build needs the results of the query. Which might not
    // have been run by our nicely lazy loaded views render array.
    $view = Views::getView(Directory::CHANNEL_VIEW);
    $view->setArguments([$node->id()]);
    $views_display = self::determineChannelViewDisplay($node);
    $view->execute($views_display);

    if (!empty($view->result)) {
      $facet_id = self::determineFacetForChannel($node);
      $block = $this->pluginBlockManager->createInstance('facet_block' . PluginBase::DERIVATIVE_SEPARATOR . $facet_id);
      return $block->build();
    }
    else {
      return [];
    }
  }

  /**
   * Retrieves the search blocks from the view for directories.
   */
  protected function getSearchBlock(NodeInterface $node) {
    $forms = $form_list = [];
    foreach ($node->localgov_directory_channels as $delta => $channel) {
      $view = Views::getView(Directory::CHANNEL_VIEW);
      if ($view && ($channel_node = $channel->entity)) {
        $views_display = self::determineChannelViewDisplay($channel_node);
        $view->setDisplay($views_display);
        $view->setArguments([$channel_node->id()]);
        $view->initHandlers();
        $form_state = (new FormState())
          ->setStorage([
            'view' => $view,
            'display' => &$view->display_handler->display,
            'rerender' => NULL,
          ])
          ->setMethod('get')
          ->setAlwaysProcess()
          ->disableRedirect();
        $form = $this->formBuilder->buildForm('\Drupal\views\Form\ViewsExposedForm', $form_state);
        $form['#action'] = $channel_node->toUrl()->toString();
        $form['#attributes']['class'][] = $delta ? 'localgov-search-channel-secondary' : 'localgov-search-channel-primary';
        $channel_label = $this->entityRepository->getTranslationFromContext($channel_node)->label();
        $form['#id'] .= '--' . $channel_node->id();
        $form["#info"]["filter-search_api_fulltext"]["label"] = $this->t('Search <span class="localgov-search-channel" id="@id--channel">@channel</span>', [
          '@id' => $form['#id'],
          '@channel' => $channel_label,
        ]);
        // Can we do this with the form builder?
        // Do we need to deal with date-drupal-selector?
        // Questions for search_api_autocomplete?
        $form_list[$form['#id']] = $channel_label;
        $form['#attached']['library'][] = 'localgov_directories/localgov_directories_search';
        $forms[] = $form;
      }
      $forms['#attached']['drupalSettings']['localgovDirectories']['directoriesSearch'] = $form_list;
    }

    return $forms;
  }

  /**
   * Prepares variables for our bundle grouped facets item list template.
   *
   * Facet bundles are sorted based on their weight.
   *
   * @see templates/facets-item-list--links--localgov-directories-facets.tpl.php
   * @see localgov_directories_preprocess_facets_item_list()
   */
  public function preprocessFacetList(array &$variables) {
    $reset_all = NULL;
    $show_reset_link = [];

    // Check if the show reset link has been enabled.
    foreach ($variables['items'] as $key => $item) {
      if ($variables["items"][$key]["value"]["#attributes"]["data-drupal-facet-item-value"] == 'reset_all') {
        $variables["attributes"]["class"][] = "facet-show-reset";
        $show_reset_link = current($variables["items"]);
      }
    }

    $variables['items'] = $this->groupDirFacetItems($variables['items']);

    if (!empty($show_reset_link)) {
      // Add the reset link.
      $variables['items']['show_reset_all']['items'][] = $show_reset_link;
      $reset_all = $variables['items']['show_reset_all'];
      // Place the reset link at the top of the facet filters.
      array_unshift($variables['items'], $reset_all);
      array_pop($variables['items']);
    }
  }

  /**
   * Groups facet checkboxes.
   *
   * Prepares variables for facet checkboxes grouped by LocalGov Directory facet
   * types.
   *
   * @see templates/checkboxes--localgov-directories-facets.html.twig
   * @see template_preprocess_checkboxes__localgov_directories_facets()
   */
  public function preprocessFacetCheckboxes(array &$variables): void {

    $facet_id_list = Element::children($variables['element']);
    $facet_options = array_filter($variables['element'], fn($facet_id) => in_array($facet_id, $facet_id_list, strict: TRUE), ARRAY_FILTER_USE_KEY);
    $variables['grouped_options'] = $this->groupDirFacetItems($facet_options);
  }

  /**
   * Groups facet items by LocalGov Directory facet types.
   */
  public function groupDirFacetItems(array $facet_items): array {

    $facet_storage = $this->entityTypeManager
      ->getStorage(Directory::FACET_CONFIG_ENTITY_ID);
    $group_items = [];
    foreach ($facet_items as $key => $item) {
      $facet_id = $item['value']['#attributes']['data-drupal-facet-item-value'] ?? $key;
      if ($facet_entity = $facet_storage->load($facet_id)) {
        assert($facet_entity instanceof LocalgovDirectoriesFacets);
        $group_items[$facet_entity->bundle()]['items'][$key] = $item;
      }
    }

    // This is usually on a channel node. If so remove facets not active on
    // channel.
    $active_facets = NULL;
    if (($channel = $this->routeMatch->getParameter('node'))
      && $channel instanceof NodeInterface
      && $channel->bundle() == 'localgov_directory'
    ) {
      $active_facets = array_column($channel->localgov_directory_facets_enable->getValue(), 'target_id');
    }
    if (!is_null($active_facets)) {
      $group_items = array_intersect_key($group_items, array_flip($active_facets));
    }

    $type_storage = $this->entityTypeManager
      ->getStorage(Directory::FACET_TYPE_CONFIG_ENTITY_ID);
    foreach ($group_items as $bundle => $items) {
      $facet_type_entity = $type_storage->load($bundle);
      assert($facet_type_entity instanceof LocalgovDirectoriesFacetsType);
      $group_items[$bundle]['title'] = Html::escape($this->entityRepository->getTranslationFromContext($facet_type_entity)->label());
      $group_items[$bundle]['weight'] = $facet_type_entity->get('weight');
    }
    uasort($group_items, static::compareFacetBundlesByWeight(...));

    return $group_items;
  }

  /**
   * Facet bundle comparison callback for sorting.
   *
   * Bundles are compared by their weights.  When weights are equal, labels take
   * over.
   *
   * @param array $bundle1
   *   Necessary keys: weight, title.
   * @param array $bundle2
   *   Same as $bundle1.
   */
  public static function compareFacetBundlesByWeight(array $bundle1, array $bundle2): int {

    if ($bundle1['weight'] === $bundle2['weight']) {
      return strnatcasecmp($bundle1['title'], $bundle2['title']);
    }

    return $bundle1['weight'] < $bundle2['weight'] ? -1 : 1;
  }

  /**
   * Finds the relevant Views display.
   *
   * Determines if the given directory channel needs the usual Views display or
   * a proximity search display.
   */
  public static function determineChannelViewDisplay(NodeInterface $channel_node): string {

    $has_proximity_search = $channel_node->hasField(Directory::PROXIMITY_SEARCH_CFG_FIELD) && !empty($channel_node->{Directory::PROXIMITY_SEARCH_CFG_FIELD}->value);
    $views_display = $has_proximity_search ? Directory::CHANNEL_VIEW_PROXIMITY_SEARCH_DISPLAY : Directory::CHANNEL_VIEW_DISPLAY;
    return $views_display;
  }

  /**
   * Finds the relevant Facet for a directory channel.
   *
   * Channels use different Views displays depending on whether proximity search
   * is in use or not.  The directory related Facets are attached to these Views
   * displays.  This means the choice of Facet differs depending on the use of
   * proximity search.
   */
  public static function determineFacetForChannel(NodeInterface $channel_node): string {

    $has_proximity_search = $channel_node->hasField(Directory::PROXIMITY_SEARCH_CFG_FIELD) && !empty($channel_node->{Directory::PROXIMITY_SEARCH_CFG_FIELD}->value);
    $facet_id = $has_proximity_search ? Directory::FACET_CONFIG_ENTITY_ID_FOR_PROXIMITY_SEARCH : Directory::FACET_CONFIG_ENTITY_ID;
    return $facet_id;
  }

  /**
   * Post render callback.
   *
   * @see ::getViewEmbed()
   */
  public static function removeExposedFilter(Markup $markup, array $render) {
    // Sure there must be a better way in the pre_render to stop it adding the
    // form, while accepting the parameters. But this does the same later.
    return $markup::create(preg_replace('|<form.*?class="[^"]*views-exposed-form.*?>.*?</form>|s', '', $markup, 1));
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc