custom_field-1.0.x-dev/src/Plugin/CustomField/FieldWidget/HierarchicalSelectWidget.php

src/Plugin/CustomField/FieldWidget/HierarchicalSelectWidget.php
<?php

declare(strict_types=1);

namespace Drupal\custom_field\Plugin\CustomField\FieldWidget;

use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\custom_field\Attribute\CustomFieldWidget;
use Drupal\custom_field\Plugin\CustomField\FieldType\EntityReference;
use Drupal\custom_field\Plugin\CustomFieldTypeInterface;

/**
 * Plugin implementation of the 'hierarchical_select' widget.
 */
#[CustomFieldWidget(
  id: 'hierarchical_select',
  label: new TranslatableMarkup('Hierarchical select'),
  category: new TranslatableMarkup('Reference'),
  field_types: [
    'entity_reference',
  ],
)]
class HierarchicalSelectWidget extends EntityReferenceWidgetBase {

  /**
   * {@inheritdoc}
   */
  public static function defaultSettings(): array {
    return [
      'force_deepest_level' => FALSE,
      'level_labels' => TRUE,
    ] + parent::defaultSettings();
  }

  /**
   * {@inheritdoc}
   */
  public function widgetSettingsForm(FormStateInterface $form_state, CustomFieldTypeInterface $field): array {
    $element = parent::widgetSettingsForm($form_state, $field);
    $settings = $this->getSettings() + static::defaultSettings();

    $element['force_deepest_level'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Force deepest level'),
      '#description' => $this->t('This will require the deepest level in the term tree to be selected.'),
      '#default_value' => $settings['force_deepest_level'],
    ];
    $element['level_labels'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Show level labels'),
      '#description' => $this->t('Show labels above widgets. The first level will be the field label and child levels will be the parent term for that level.'),
      '#default_value' => $settings['level_labels'],
    ];

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function widget(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state, CustomFieldTypeInterface $field): array {
    $element = parent::widget($items, $delta, $element, $form, $form_state, $field);
    assert($field instanceof EntityReference);
    $field_settings = $field->getFieldSettings();
    $settings = $this->getSettings() + static::defaultSettings();
    /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
    $term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
    /** @var \Drupal\Core\Field\FieldItemInterface $item */
    $item = $items[$delta];
    $langcode = $item->getEntity()->language()->getId();
    $field_name = $item->getFieldDefinition()->getName();
    $name = $field->getName();
    $parents = $form['#parents'] ?? [];
    $wrapper = $this->getUniqueElementId($form, $field_name, $delta, $name);
    $translations_enabled = $this->moduleHandler->moduleExists('content_translation');
    $target_type = $field->getTargetType();

    // Account for parents structure from paragraphs field if applicable.
    $value_keys = array_merge($parents, [$field_name, $delta, $name]);
    $values = $form_state->getValues();
    $field_value = NestedArray::getValue($values, $value_keys);

    // If there are no processed values, use the input.
    if (empty($field_value)) {
      $user_input = $form_state->getUserInput();
      $field_value = NestedArray::getValue($user_input, $value_keys);
    }
    if (!empty($field_value)) {
      $levels = $field_value['levels'] ?? [];
    }
    // Use the saved values.
    else {
      $levels = !empty($item->{$name}) ? $this->getPathToRoot((int) $item->{$name}) : [];
    }

    $handler = $field->getSelectionHandler($field_settings, $target_type);
    $base_options = [];
    $views_enabled = $this->moduleHandler->moduleExists('views');
    if ($views_enabled && $handler::class === 'Drupal\views\Plugin\EntityReferenceSelection\ViewsSelection') {
      $configuration = $handler->configuration;
      // Return early if the view hasn't been selected.
      if (empty($configuration['view']['view_name'])) {
        return $element;
      }
      try {
        /** @var \Drupal\views\Entity\View $view */
        $view = $this->entityTypeManager->getStorage('view')->load($configuration['view']['view_name']);
        $limit = 0;
        if ($display = $view->getDisplay($configuration['view']['display_name'])) {
          // If the views display has a limit set, use it.
          if (isset($display['display_options']['pager']['options']['items_per_page'])) {
            $limit = $display['display_options']['pager']['options']['items_per_page'];
          }
        }
        $views_options = $handler->getReferenceableEntities(NULL, 'CONTAINS', $limit);
        foreach ($views_options as $category => $terms) {
          /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
          $vocabulary = $this->entityTypeManager->getStorage('taxonomy_vocabulary')->load($category);
          foreach ($terms as $id => $term) {
            $base_options[(string) $vocabulary->label()][$id] = $term;
          }
        }
      }
      catch (InvalidPluginDefinitionException | PluginNotFoundException $e) {
        return $element;
      }
    }
    else {
      $target_bundles = $field_settings['handler_settings']['target_bundles'] ?? [];
      if (empty($target_bundles)) {
        return $element;
      }
      $base_options = $this->getBaseOptions($target_bundles, $langcode, $translations_enabled);
    }

    $title = $element['#title'];
    // Unset the title so we can display it inline for the level.
    unset($element['#title']);
    if ($settings['level_labels']) {
      $element['#wrapper_attributes']['class'][] = 'custom-field-level-labels';
    }
    $element['#type'] = 'item';
    $element['#tree'] = TRUE;
    $element['levels'] = [
      '#type' => 'container',
      '#attributes' => [
        'id' => $wrapper,
        'class' => ['custom-field-levels-wrapper'],
      ],
    ];
    // Always display the top level term field.
    $element['levels'][0] = [
      '#type' => 'select',
      '#title' => $title,
      '#options' => $base_options,
      '#empty_option' => $this->t('- Select -'),
      '#default_value' => $levels[0] ?? NULL,
      '#required' => $element['#required'],
      '#ajax' => [
        'callback' => [$this, 'ajaxLoadNextLevel'],
        'wrapper' => $wrapper,
        'event' => 'change',
      ],
    ];
    foreach ($levels as $key => $level) {
      /** @var \Drupal\taxonomy\TermInterface $parent */
      if (!empty($level) && $parent = $term_storage->load($level)) {
        $child_options = $this->getOptionsForLevel($parent, $langcode, $translations_enabled);
        $level_key = $key + 1;
        $level_value = $levels[$level_key] ?? NULL;
        // Add a new level for each parent level that has child terms.
        if (!empty($child_options)) {
          $element['levels'][$level_key] = [
            '#type' => 'select',
            '#title' => $this->t('@term', ['@term' => $parent->getName()]),
            '#title_display' => $settings['level_labels'] ? 'before' : 'invisible',
            '#options' => $child_options,
            '#empty_option' => $this->t('- Select -'),
            '#default_value' => $level_value,
            '#required' => $settings['force_deepest_level'] ?? FALSE,
            '#ajax' => [
              'callback' => [$this, 'ajaxLoadNextLevel'],
              'wrapper' => $wrapper,
              'event' => 'change',
            ],
            '#element_validate' => [[$this, 'validateLevel']],
          ];
          // If the top level changed, unset the child level value.
          if (!empty($level_value) && !isset($child_options[$level_value])) {
            $element['levels'][$level_key]['#value'] = NULL;
          }
        }
      }
    }

    return $element;
  }

  /**
   * The #element_validate callback for levels.
   *
   * @param array $element
   *   An associative array containing the properties and children of the
   *   generic form element.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form for the form this element belongs to.
   *
   * @see \Drupal\Core\Render\Element\FormElement::processPattern()
   */
  public function validateLevel(array $element, FormStateInterface $form_state): void {
    /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
    $term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
    $parents = array_slice($element['#parents'], 0, -1);
    $values = $form_state->getValues();
    $user_input = $form_state->getUserInput();
    $levels = NestedArray::getValue($values, $parents);
    $error_name = implode('][', $element['#parents']);
    $errors = $form_state->getErrors();
    // Clear the errors to prevent stale values that don't exist in options.
    $form_state->clearErrors();
    if (isset($errors[$error_name])) {
      // Unset the error for our widget.
      unset($errors[$error_name]);
    }
    // Now loop through and re-apply the remaining form error messages.
    foreach ($errors as $name => $error_message) {
      $form_state->setErrorByName($name, $error_message);
    }
    foreach ($levels as $key => $level) {
      // Skip the root level.
      if ($key === 0) {
        continue;
      }
      if (!empty($level)) {
        // The parent id will be 1 level up.
        $parent_id = $levels[$key - 1];
        // Check if the parent level term id exists in the level's parents.
        $parent_terms = array_keys($term_storage->loadAllParents($level));
        if (!in_array($parent_id, $parent_terms)) {
          unset($levels[$key]);
        }
      }
    }
    // Update values and input.
    NestedArray::setValue($values, $parents, $levels);
    NestedArray::setValue($user_input, $parents, $levels);
  }

  /**
   * {@inheritdoc}
   */
  public static function isApplicable(CustomFieldTypeInterface $custom_item): bool {
    return $custom_item->getTargetType() === 'taxonomy_term';
  }

  /**
   * Ajax callback to retrieve levels.
   *
   * @param array $form
   *   The form from which the display IDs are being requested.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the form.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The Ajax response.
   */
  public function ajaxLoadNextLevel(array &$form, FormStateInterface $form_state): AjaxResponse {
    $trigger = $form_state->getTriggeringElement();
    $wrapper_id = $trigger['#ajax']['wrapper'];
    $form_state_keys = array_slice($trigger['#array_parents'], 0, -1);

    // Get the updated element from the form structure.
    $updated_element = NestedArray::getValue($form, $form_state_keys);
    $children = Element::children($updated_element);

    $response = new AjaxResponse();
    // Add a ReplaceCommand to replace the content inside the widget's wrapper.
    $response->addCommand(new ReplaceCommand('#' . $wrapper_id, $updated_element));
    if (count($children) > 1) {
      $ids = array_keys($response->getAttachments()['drupalSettings']['ajax'] ?? []);
      if (!empty($ids)) {
        // The first item in the array is actually the last triggered.
        $focus_id = reset($ids);
        $response->addCommand(new InvokeCommand('#' . $focus_id, 'focus'));
      }
    }
    $form_state->setRebuild();

    return $response;
  }

  /**
   * Gets the path from a term to the root of the taxonomy tree.
   *
   * @param int|null $tid
   *   The term ID.
   *
   * @return array
   *   An array containing the term IDs from root to the given term.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  protected function getPathToRoot(?int $tid): array {
    if (!$tid) {
      return [];
    }
    /** @var \Drupal\taxonomy\TermInterface $term */
    $term = $this->entityTypeManager->getStorage('taxonomy_term')->load($tid);
    if ($term) {
      $parent_tid = $term->get('parent')->target_id;
      if ($parent_tid != 0) {
        return array_merge($this->getPathToRoot((int) $parent_tid), [$tid]);
      }
    }
    return [$tid];
  }

  /**
   * Helper function to get top level term options.
   *
   * @param string[] $target_bundles
   *   The enabled target bundles.
   * @param string $langcode
   *   The language code.
   * @param bool $translations_enabled
   *   The 'content_translation' module is enabled.
   *
   * @return array
   *   The first level options.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  protected function getBaseOptions(array $target_bundles, string $langcode, bool $translations_enabled): array {
    $has_admin_access = $this->currentUser->hasPermission('administer taxonomy');
    /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
    $term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
    $options = [];
    foreach ($target_bundles as $target_bundle) {
      /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
      if ($vocabulary = $this->entityTypeManager->getStorage('taxonomy_vocabulary')->load($target_bundle)) {
        /** @var \Drupal\taxonomy\TermInterface[]|object[] $terms */
        if ($terms = $term_storage->loadTree((string) $vocabulary->id(), 0, 1, $translations_enabled)) {
          foreach ($terms as $term) {
            if ($translations_enabled && ($term instanceof TranslatableInterface) && $term->hasTranslation($langcode)) {
              $term = $term->getTranslation($langcode);
              $tid = $term->id();
              $published = method_exists($term, 'isPublished') && $term->isPublished();
              $label = $term->label();
            }
            else {
              // @phpstan-ignore property.notFound
              $tid = $term->tid;
              // @phpstan-ignore property.notFound
              $published = $term->status;
              // @phpstan-ignore property.notFound
              $label = $term->name;
            }
            if (!$has_admin_access && !$published) {
              continue;
            }
            $options[(string) $vocabulary->label()][$tid] = $label;
          }
        }
      }
    }
    if (count($options) === 1) {
      $options = reset($options);
    }

    return $options;
  }

  /**
   * Helper function to return options array from child terms.
   *
   * @param \Drupal\taxonomy\TermInterface $parent
   *   The parent term.
   * @param string $langcode
   *   The language code.
   * @param bool $translations_enabled
   *   The 'content_translation' module is enabled.
   *
   * @return array<int|string, string>
   *   The options.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  protected function getOptionsForLevel($parent, string $langcode, bool $translations_enabled): array {
    /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */
    $term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
    $has_admin_access = $this->currentUser->hasPermission('administer taxonomy');
    $vocabulary = $parent->bundle();
    $options = [];
    if ($terms = $term_storage->loadTree($vocabulary, (int) $parent->id(), 1, $translations_enabled)) {
      foreach ($terms as $term) {
        if ($translations_enabled && ($term instanceof TranslatableInterface) && $term->hasTranslation($langcode)) {
          $term = $term->getTranslation($langcode);
          $tid = $term->id();
          $published = method_exists($term, 'isPublished') && $term->isPublished();
          $label = $term->label();
        }
        else {
          // @phpstan-ignore property.notFound
          $tid = $term->tid;
          // @phpstan-ignore property.notFound
          $published = (bool) $term->status;
          // @phpstan-ignore property.notFound
          $label = $term->name;
        }
        if (!$has_admin_access && !$published) {
          continue;
        }
        $options[$tid] = $label;
      }
    }

    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function massageFormValue(mixed $value, array $column): ?array {
    $levels = $value['levels'] ? array_filter($value['levels']) : [];
    if (empty($levels)) {
      return NULL;
    }

    return ['target_id' => end($levels)];
  }

}

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

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