toolshed-8.x-1.x-dev/modules/toolshed_search/src/Plugin/Block/ToolshedSearchBlock.php

modules/toolshed_search/src/Plugin/Block/ToolshedSearchBlock.php
<?php

namespace Drupal\toolshed_search\Plugin\Block;

use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SortArray;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\toolshed_search\Form\ToolshedSearchFilterForm;
use Drupal\toolshed_search\Plugin\ToolshedSearchAutocompleteInterface;
use Drupal\toolshed_search\Plugin\ToolshedSearchAutocompletePluginManager;
use Drupal\views\ViewExecutable;
use Drupal\views\ViewExecutableFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;

/**
 * Block for creating configurable exposed search blocks.
 *
 * @Block(
 *   id = "toolshed_search_form",
 *   admin_label = @Translation("Toolshed: Search block"),
 *   category = @Translation("Search API"),
 * )
 */
class ToolshedSearchBlock extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * The loaded view or FALSE if the view is no longer available.
   *
   * @var \Drupal\views\ViewExecutable|null|false
   */
  protected ViewExecutable|null|false $loadedView;

  /**
   * An array of configured views filters to exposed in the block form.
   *
   * @var array
   */
  protected ?array $filters;

  /**
   * Drupal form builder interface for generating form structures.
   *
   * @var \Drupal\Core\Form\FormBuilderInterface
   */
  protected FormBuilderInterface $formBuilder;

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

  /**
   * Creates executatable views from views entity config instances.
   *
   * @var \Drupal\views\ViewExecutableFactory
   */
  protected ViewExecutableFactory $viewFactory;

  /**
   * The search autocomplete plugin manager.
   *
   * @var \Drupal\toolshed_search\Plugin\ToolshedSearchAutocompletePluginManager
   */
  protected ToolshedSearchAutocompletePluginManager $autocompleteManager;

  /**
   * Create a new instance of the toolshed search block.
   *
   * @param array $configuration
   *   The plugin configurations for the block setup.
   * @param string $plugin_id
   *   The unique ID of the plugin.
   * @param mixed $plugin_definition
   *   The plugin definition from the plugin discovery.
   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
   *   The Drupal form builder service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\views\ViewExecutableFactory $view_executable_factory
   *   Creates executatable views from views entity config instances.
   * @param \Drupal\toolshed_search\Plugin\ToolshedSearchAutocompletePluginManager $autocomplete_manager
   *   The search autocomplete plugin manager.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder, EntityTypeManagerInterface $entity_type_manager, ViewExecutableFactory $view_executable_factory, ToolshedSearchAutocompletePluginManager $autocomplete_manager) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);

    $this->formBuilder = $form_builder;
    $this->entityTypeManager = $entity_type_manager;
    $this->viewFactory = $view_executable_factory;
    $this->autocompleteManager = $autocomplete_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('form_builder'),
      $container->get('entity_type.manager'),
      $container->get('views.executable'),
      $container->get('plugin.manager.toolshed_search_autocomplete')
    );
  }

  /**
   * Retrieve the view targeted by this block.
   *
   * @return \Drupal\views\ViewExecutable|false
   *   The view entity loaded, with the proper display configured. Will return
   *   NULL if the view or the display are no longer available.
   */
  public function getView(): ViewExecutable|false {
    if (!isset($this->loadedView)) {
      $this->loadedView = $this->loadView($this->configuration['view']) ?: FALSE;
    }

    return $this->loadedView;
  }

  /**
   * {@inheritdoc}
   */
  public function blockAccess(AccountInterface $account): AccessResultInterface {
    if ($view = $this->getView()) {
      $access = $view->access($view->current_display, $account);
      return AccessResult::allowedIf($access);
    }

    return AccessResult::forbidden();
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags(): array {
    $tags = parent::getCacheTags();

    if ($view = $this->getView()) {
      $viewTags = $view->display_handler
        ->getCacheMetadata()
        ->getCacheTags();

      $tags = Cache::mergeTags($viewTags, $tags);
    }

    return $tags;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts(): array {
    $contexts = parent::getCacheContexts();

    if ($view = $this->getView()) {
      $viewContexts = $view->display_handler
        ->getCacheMetadata()
        ->getCacheContexts();

      $contexts = Cache::mergeContexts($viewContexts, $contexts);

      // Add cache contexts from each of the configured views filters.
      foreach ($this->getFilters() as $data) {
        /** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $filter */
        $filter = $data['filter'];
        $contexts = Cache::mergeContexts($filter->getCacheContexts(), $contexts);
      }
    }

    return $contexts;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration(): array {
    return [
      'view' => ':',
      'autocomplete' => '',
      'filters' => [],
      'use_exposed_input' => TRUE,
      'grouped_filter' => FALSE,
      'action' => 'view_url',
      'submit_label' => $this->t('Search'),
      'submit_class_names' => [],
      'reset_link' => FALSE,
      'reset_link_text' => $this->t('Reset filters'),
      'variant' => NULL,
    ] + parent::defaultConfiguration();
  }

  /**
   * {@inheritdoc}
   */
  public function build(): array {
    $build = [];

    if (!empty($this->configuration['filters']) && $view = $this->getView()) {
      $state = new FormState();
      $state
        ->setMethod('GET')
        ->setRequestMethod('GET')
        ->disableCache()
        ->disableRedirect()
        ->setAlwaysProcess()
        ->addBuildInfo('args', [$this])
        ->setStorage([
          'view' => $view,
          'display' => &$view->display_handler->display,
          'rerender' => TRUE,
        ]);

      $intro = $this->configuration['intro_content'] ?? [];
      if (!empty($intro['value'])) {
        $build['intro_content'] = [
          '#theme_wrappers' => ['container'],
          '#type' => 'processed_text',
          '#text' => $intro['value'],
          '#format' => $intro['format'] ?? 'plain_text',
          '#attributes' => [
            'class' => ['search-block__intro'],
          ],
        ];
      }

      $build['form'] = $this->formBuilder->buildForm(ToolshedSearchFilterForm::class, $state);
    }

    return $build;
  }

  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state): array {
    $config = $this->getConfiguration();
    $parentState = $form_state instanceof SubformState ? $form_state->getCompleteFormState() : $form_state;

    $viewId = $parentState->getValue(['settings', 'view']);
    $viewId = !isset($viewId) ? $config['view'] : $viewId;
    $viewOptions = $this->getViewOptions();

    $form['view'] = [
      '#type' => 'select',
      '#title' => $this->t('View to run search'),
      '#required' => TRUE,
      '#options' => $viewOptions,
      '#default_value' => $viewId,
      '#description' => $this->t('Only search_api views with a page display appear in this list.'),
      '#ajax' => [
        'wrapper' => 'views-display-ajax-wrapper',
        'callback' => static::class . '::ajaxUpdateViewDisplays',
      ],
    ];

    $form['filters'] = [
      '#prefix' => '<div id="views-display-ajax-wrapper">',
      '#suffix' => '</div>',
    ];

    if (isset($viewOptions[$viewId]) && ($view = $this->loadView($viewId))) {
      $form['filters'] = $this->buildFilterFormElements($view);
      $form['autocomplete'] = $this->buildAutocompleteElements($view);
    }

    $form['intro_content'] = [
      '#type' => 'text_format',
      '#title' => $this->t('Intro content'),
      '#rows' => 3,
      '#format' => $config['intro_content']['format'] ?? NULL,
      '#default_value' => $config['intro_content']['value'] ?? NULL,
    ];

    $form['use_exposed_input'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Apply exposed input values'),
      '#default_value' => $config['use_exposed_input'] ?? FALSE,
      '#description' => $this->t('Checking this applies the current exposed input values from the request to the form elements. Otherwise elements will always be populate by the View defaults.'),
    ];

    $form['grouped_filter'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Filter combine with other filter blocks'),
      '#default_value' => $config['grouped_filter'] ?? FALSE,
      '#description' => $this->t('Filters in this block will combine with other filters that target the same view.'),
    ];

    $form['submit_label'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Submit button text'),
      '#default_value' => $config['submit_label'],
    ];

    $form['submit_class_names'] = [
      '#type' => 'css_class',
      '#title' => $this->t('Button class names'),
      '#default_value' => $config['submit_class_names'],
      '#description' => $this->t('Additional CSS class names to apply to the submit button.'),
    ];

    $form['reset_link'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Include reset filters'),
      '#default_value' => $config['reset_link'],
    ];

    $form['reset_link_text'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Reset link text'),
      '#default_value' => $config['reset_link_text'] ?? $this->t('Reset filters'),
      '#states' => [
        'visible' => [
          'input[name="settings[reset_link]"]' => ['checked' => TRUE],
        ],
      ],
    ];

    $form['variant'] = [
      '#type' => 'textfield',
      '#title' => $this->t("Block variant"),
      '#pattern' => '^[\w_ ]*$',
      '#default_value' => $this->configuration['variant'],
      '#description' => $this->t('Optional extra label to provide info to the ARIA descriptions to provide context if the search block appears more than once on a page. (i.e. "footer" or "header")'),
    ];

    $form['action'] = [
      '#type' => 'select',
      '#title' => $this->t('Form submit target'),
      '#options' => [
        'view_url' => $this->t('The view display page'),
        'current_page' => $this->t('Submit to same page'),
        'custom_url' => $this->t('Custom URL'),
      ],
      '#default_value' => $config['action'],
      '#description' => $this->t('Custom URL is useful for page manager, or pages that embed the search view.'),
    ];

    $form['custom_url'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Custom URL to submit form to'),
      '#default_value' => $config['custom_url'] ?? '',
      '#states' => [
        'visible' => [
          'select[name="settings[action]"]' => ['value' => 'custom_url'],
        ],
        'required' => [
          'select[name="settings[action]"]' => ['value' => 'custom_url'],
        ],
      ],
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockValidate($form, FormStateInterface $form_state): void {
    $action = $form_state->getValue('action');
    if ('custom_url' === $action && !UrlHelper::isValid($form_state->getValue('custom_url'))) {
      $form_state->setError($form['custom_url'], $this->t('Custom URL path is not a valid.'));
    }
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state): void {
    $values = $form_state->getValues() + [
      'filters' => [],
    ];

    $this->configuration['view'] = $values['view'] ?? ':';
    $this->configuration['intro_content'] = $values['intro_content'];
    $this->configuration['autocomplete'] = $values['autocomplete'];
    $this->configuration['use_exposed_input'] = $values['use_exposed_input'];
    $this->configuration['grouped_filter'] = $values['grouped_filter'];
    $this->configuration['submit_label'] = trim($values['submit_label']);
    $this->configuration['submit_class_names'] = $values['submit_class_names'];
    $this->configuration['reset_link'] = $values['reset_link'];
    $this->configuration['reset_link_text'] = trim($values['reset_link_text']);
    $this->configuration['variant'] = trim($values['variant']);

    // Resort the filters by the weight.
    uasort($values['filters'], SortArray::sortByWeightElement(...));

    $this->configuration['filters'] = [];
    foreach ($values['filters'] as $name => $filterData) {
      if ($filterData['enabled']) {
        $this->configuration['filters'][$name] = [
          'label' => $filterData['label'],
          'label_display' => $filterData['label_display'],
        ];
      }
    }

    $this->configuration['action'] = $values['action'];
    if ('custom_url' === $values['action']) {
      $this->configuration['custom_url'] = $values['custom_url'];
    }
  }

  /**
   * Update the block configuration form after the view has been selected.
   *
   * @param array $form
   *   Structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form and build information.
   *
   * @return array|\Symfony\Component\HttpFoundation\Response
   *   Renderable array of content to be replaced using AJAX.
   */
  public static function ajaxUpdateViewDisplays(array $form, FormStateInterface $form_state): array|Response {
    $trigger = $form_state->getTriggeringElement();
    $parents = array_slice($trigger['#array_parents'], 0, -1);
    $parents[] = 'filters';

    return NestedArray::getValue($form, $parents);
  }

  /**
   * Get the available views which can be selected to create filter blocks for.
   *
   * @return array
   *   Array of views that can be the target of this search filters form.
   */
  protected function getViewOptions(): array {
    /** @var \Drupal\views\ViewEntityInterface[] $views */
    $views = $this->entityTypeManager
      ->getStorage('view')
      ->loadByProperties(['status' => TRUE]);

    $options = ['' => '-- Select a View --'];
    foreach ($views as $viewId => $view) {
      if ($view->get('base_field') !== 'search_api_id' && !preg_match('/^search_api_index/', $view->get('base_table'))) {
        continue;
      }

      // Only create a blocks that have search API page displays to redirect to.
      foreach ($view->get('display') as $displayId => $display) {
        if (in_array($display['display_plugin'], ['page', 'block'])) {
          $options["$viewId:$displayId"] = $view->label() . " ($display[display_title])";
        }
      }
    }

    return $options;
  }

  /**
   * Retrieve a view and display settings based a view and display ID.
   *
   * @return \Drupal\views\ViewExecutable|null
   *   The view entity loaded, with the proper display configured. Will return
   *   NULL if the view or the display are no longer available.
   */
  protected function loadView($viewName): ?ViewExecutable {
    [$viewId, $display] = explode(':', $viewName);

    if ($viewId && $display) {
      $entity = $this->entityTypeManager
        ->getStorage('view')
        ->load($viewId);

      if ($entity) {
        $view = $this->viewFactory->get($entity);
        return $view->setDisplay($display) ? $view : NULL;
      }
    }

    return NULL;
  }

  /**
   * Create the form elements for configuring the views filters.
   *
   * @param \Drupal\views\ViewExecutable $view
   *   The view to get exposed filter information from.
   *
   * @return array
   *   Form element definitions for configuring the views filters.
   */
  protected function buildFilterFormElements(ViewExecutable $view): array {
    $elements = [];
    $filterOpts = [];

    /** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $handler */
    foreach ($view->display_handler->getHandlers('filter') as $fieldName => $handler) {
      if ($handler->isExposed()) {
        $filterOpts[$fieldName] = [
          'title' => $handler->pluginTitle(),
          'label' => $handler->exposedInfo()['label'] ?? $handler->pluginTitle(),
        ];
      }
    }

    $elements = [
      '#type' => 'table',
      '#caption' => $this->t('Exposed filters'),
      '#required' => TRUE,
      '#empty' => $this->t('No filters are exposed for this View, go enable some exposed filters to have them appear here.'),
      '#header' => [
        'filter' => $this->t('Filter'),
        'label' => $this->t('Label'),
        'display' => $this->t('Label display'),
        'weight' => $this->t('Sort order'),
      ],
      '#tabledrag' => [
        [
          'action' => 'order',
          'relationship' => 'sibling',
          'group' => 'filter-weight',
        ],
      ],
    ];

    $filters = $this->configuration['filters'] + $filterOpts;
    foreach (array_intersect_key($filters, $filterOpts) as $name => $option) {
      $elements[$name] = [
        '#attributes' => [
          'class' => ['draggable'],
        ],

        'enabled' => [
          '#type' => 'checkbox',
          '#title' => $filterOpts[$name]['title'],
          '#default_value' => !empty($this->configuration['filters'][$name]),
        ],
        'label' => [
          '#type' => 'textfield',
          '#placeholder' => $filterOpts[$name]['label'],
          '#default_value' => $this->configuration['filters'][$name]['label'] ?? '',
        ],
        'label_display' => [
          '#type' => 'select',
          '#options' => [
            'before' => $this->t('Before'),
            'after' => $this->t('After'),
            'invisible' => $this->t('Visually hidden'),
          ],
          '#default_value' => $this->configuration['filters'][$name]['label_display'] ?? 'before',
        ],
        'weight' => [
          '#type' => 'number',
          '#attributes' => [
            'class' => ['filter-weight'],
          ],
        ],
      ];
    }

    return $elements;
  }

  /**
   * Create the form elements for configuring the autocomplete behavior.
   *
   * @param \Drupal\views\ViewExecutable $view
   *   The view to get autocomplete compatible functionality for.
   *
   * @return array
   *   Form elements for configuring the autocomplete behavior for this block.
   */
  protected function buildAutocompleteElements(ViewExecutable $view): array {
    $acOptions = [];
    foreach ($this->autocompleteManager->getDefinitions() as $pluginId => $value) {
      $autocomplete = $this->autocompleteManager->createInstance($pluginId, []);
      if ($autocomplete->isApplicable($view)) {
        $acOptions[$pluginId] = $value['label'];
      }
    }

    if ($acOptions) {
      return [
        '#type' => 'fieldset',
        '#title' => $this->t('Fulltext Autocomplete'),
        '#tree' => TRUE,

        'plugin' => [
          '#type' => 'select',
          '#title' => $this->t('Provider'),
          '#options' => ['' => $this->t('- None -')] + $acOptions,
          '#default_value' => $this->configuration['autocomplete']['plugin'] ?? '',
          '#description' => $this->t('Applies the search autocomplete to all the search_api fulltext filters.'),
        ],
        'config' => [
          '#type' => 'value',
          '#value' => [],
        ],
      ];
    }

    return [
      '#type' => 'value',
      '#value' => [
        'plugin' => '',
        'config' => [],
      ],
    ];
  }

  /**
   * Get the views exposed filter plugins configured to be rendred.
   *
   * @param bool $reset
   *   If true the method will always generate the list of exposed filters.
   *
   * @return array
   *   List of plugin filters which are exposed for the view and configured to
   *   be built by the block configurations.
   */
  public function getFilters($reset = FALSE): array {
    if (!isset($this->filters) || $reset) {
      $this->filters = [];

      if ($view = $this->getView()) {
        $config = $this->getConfiguration();
        $filters = $view->display_handler->getHandlers('filter');

        foreach ($config['filters'] as $name => $info) {
          /** @var \Drupal\views\Plugin\views\filter\FilterPluginBase|null $filter */
          $filter = $filters[$name] ?? NULL;

          if ($filter?->canExpose() && $filter->isExposed()) {
            $this->filters[$name] = [
              'label' => $info['label'] ?? NULL,
              'label_display' => $info['label_display'] ?? 'before',
              'filter' => $filter,
            ];
          }
        }
      }
    }

    return $this->filters;
  }

  /**
   * Gets a loaded instance of the search autocomplete plugin for this block.
   *
   * @return \Drupal\toolshed_search\Plugin\ToolshedSearchAutocompleteInterface|null
   *   The search autocomplete plugin if one was configured for this block.
   */
  public function getAutocomplete(): ?ToolshedSearchAutocompleteInterface {
    try {
      if (!empty($this->configuration['autocomplete']['plugin'])) {
        ['plugin' => $pluginId, 'config' => $config] = $this->configuration['autocomplete'] + [
          'config' => [],
        ];

        return $this->autocompleteManager->createInstance($pluginId, $config);
      }
    }
    catch (PluginNotFoundException) {
      // Plugin missing, just treat this as no autocomplete available.
    }

    return NULL;
  }

}

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

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