fieldry-1.0.x-dev/src/FieldItemRemovalElement.php

src/FieldItemRemovalElement.php
<?php

namespace Drupal\fieldry;

use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\WidgetInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Utility class for encapsulating a field item removal button.
 */
class FieldItemRemovalElement {

  use StringTranslationTrait;

  /**
   * Create a new instance of the FieldItemRemovalElement helper object.
   *
   * @param string $fieldName
   *   The name of the field being worked on.
   * @param class-string $widgetClass
   *   The field widget plugin class for building and processing the field
   *   edit form elements on entity forms.
   */
  public function __construct(protected string $fieldName, protected $widgetClass) {
  }

  /**
   * Determine if the field removal actions should apply to this widget.
   *
   * @param \Drupal\Core\Field\WidgetInterface $widget
   *   Field widget for editing the field.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   Field definition of the field being checked if it is compatible.
   *
   * @return bool
   *   TRUE if the field item removal functionality is available for this field
   *   and field widget.
   */
  public static function isApplicable(WidgetInterface $widget, FieldDefinitionInterface $field_definition): bool {
    $fieldStorage = $field_definition->getFieldStorageDefinition();

    if ($fieldStorage->getCardinality() === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {

      // Paragraphs have their own implementations of field item removal.
      //
      // @todo Create way to detect other instances which implement their own
      // field item removal. These can be disabled using the field widget
      // configuration, but ideally these would be identified and skipped.
      if ($fieldStorage->getSetting('target_type') === 'paragraph' || $field_definition->getTargetEntityTypeId() === 'entity_subqueue') {
        return FALSE;
      }

      return !$field_definition->isComputed() && empty($widget->getPluginDefinition()['multiple_values']);
    }

    return FALSE;
  }

  /**
   * Get the current settings for the field removal button.
   *
   * @param \Drupal\Core\Field\WidgetInterface $widget
   *   Field widget for editing the field.
   *
   * @return bool
   *   TRUE if the field item removal functionality is enabled for this widget,
   *   otherwise return FALSE.
   */
  public static function isEnabled(WidgetInterface $widget): bool {
    return (bool) ($widget->getThirdPartySetting('fieldry', 'field_item_removal') ?? TRUE);
  }

  /**
   * Build the form settings form for the field item removal buttons.
   *
   * @param \Drupal\Core\Field\WidgetInterface $widget
   *   Field widget for editing the field.
   *
   * @return array
   *   Form elements for configuring the field removal button settings.
   */
  public static function buildSettingsForm(WidgetInterface $widget) {
    return [
      'field_item_removal' => [
        '#type' => 'checkbox',
        '#title' => t('Include a button for removing field items'),
        '#default_value' => static::isEnabled($widget),
      ],
    ];
  }

  /**
   * Get the array of items that are the form element parent keys.
   *
   * @param array $element
   *   Array containing the form structure of the field widget form elements.
   *
   * @return string[]
   *   An array of array index keys which act as a trail through the field
   *   field parent structure. Should be similar to the normal form['#parents'].
   */
  protected static function getFieldParents(array $element): array {
    $parents = [];

    if (isset($element['#field_parents'])) {
      return $element['#field_parents'];
    }
    elseif (isset($element['#parents'])) {
      return $element['#parents'];
    }
    else {
      foreach (Element::children($element) as $child) {
        if (isset($element[$child]['#field_parents'])) {
          return $element[$child]['#field_parents'];
        }
      }
    }

    return $parents;
  }

  /**
   * Create an ID for the form element wrapper in a consistent way.
   *
   * @param array $parents
   *   Either the field parents or element value parents as an array.
   *
   * @return string
   *   An HTML cleaned CSS identifier based on the field and its parents.
   */
  protected function getElementId(array $parents): string {
    $parents[] = $this->fieldName;

    return Html::cleanCssIdentifier(implode('-', $parents));
  }

  /**
   * Add remove buttons to each of the field items.
   *
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   List of field items available for this field.
   * @param array $element
   *   The widget form element for editing these field items.
   */
  public function addRemoveButtons(FieldItemListInterface $items, array &$element): void {
    $parents = static::getFieldParents($element);
    $elementId = $this->getElementId($parents);

    // Try to detect the HTML wrapper ID attrribute, otherwise build one.
    $wrapperId = preg_match('/\s*id=("|\')([\w_-]+)\1/', $element['#prefix'], $matches)
      ? $matches[2] : $elementId . '-add-more-wrapper';

    // For each of the field items add a button control to remove that item.
    foreach ($items as $delta => $item) {
      if (empty($element[$delta])) {
        continue;
      }

      $element[$delta]['fieldry_item_removal'] = [
        '#theme_wrappers' => ['html_button__fieldry_item_remove'],
        '#type' => 'html_button',
        '#name' => Html::cleanCssIdentifier("{$elementId}-remove-{$delta}"),
        '#executes_submit_callback' => TRUE,
        '#field_delta' => $delta,
        '#value' => $this->t('Remove'),
        '#weight' => 99,
        '#limit_validation_errors' => [],
        '#submit' => [[$this, 'submitFieldRemoval']],
        '#ajax' => [
          'callback' => static::class . '::ajaxFieldRemoval',
          'wrapper' => $wrapperId,
        ],

        'content' => [
          '#markup' => $this->t('Remove'),
        ],
      ];
    }
  }

  /**
   * Form submit callback for removing a specific field delta.
   *
   * @param array &$form
   *   Full structure of the built Drupal form being submitted.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   State and storage for the form values and metadata.
   */
  public function submitFieldRemoval(array &$form, FormStateInterface $form_state): void {
    $fieldName = $this->fieldName;
    $trigger = $form_state->getTriggeringElement();
    $delta = $trigger['#field_delta'];
    $parents = array_slice($trigger['#array_parents'], 0, -2);
    $element = &NestedArray::getValue($form, $parents);

    $fieldParents = static::getFieldParents($element);
    $widgetState = $this->widgetClass::getWidgetState($fieldParents, $fieldName, $form_state);

    $input = &$form_state->getUserInput();
    $values = NestedArray::getValue($input, $element['#parents']);
    unset($element[$delta], $values[$delta]);

    if (!empty($widgetState['items_count'])) {
      --$widgetState['items_count'];
      /** @var \Drupal\Core\Entity\EntityForm $entityForm */
      $entityForm = $form_state->getFormObject();
      $items = $entityForm->getEntity()->{$fieldName};

      // When going from 1 item to 0, Drupal will try to rebuild this widget
      // with the entity field values, which causes the field to get repopulated
      // when after removing all items this widget should be empty.
      if (empty($widgetState['items_count'])) {
        $items->setValue([]);
        unset($widgetState['original_deltas']);
      }
      else {
        $items->removeItem($delta);

        // Attempt to keep the information about the origin deltas of field
        // items as this helps with some complex fields like entity references.
        $deltas = [];
        foreach ($values as $key => $value) {
          $deltas[] = $widgetState['original_deltas'][$key] ?? NULL;
        }
        $widgetState['original_deltas'] = array_filter($deltas);
      }

      $this->widgetClass::setWidgetState($fieldParents, $fieldName, $form_state, $widgetState);
    }

    // Reset the item weights to avoid having an invalid weight value that
    // causes items to shift order after widget rebuild. The max allowed
    // weight value is equal to the number of items (which we just changed).
    //
    // EX: 3 items, largest weight is 3 - when 1 item is removed, largest
    // allowed becomes 2. So any weight value of 3 is now invalid, and the
    // number now overflows to -2 (the new lowest) and is thrown to the top.
    $cleaned = [];
    $weight = 0;
    foreach ($values as $value) {
      $value['_weight'] = $weight++;
      $cleaned[] = $value;
    }
    NestedArray::setValue($input, $element['#parents'], $cleaned, TRUE);

    $form_state->setRebuild(TRUE);
  }

  /**
   * Form AJAX callback for the removal of a field item.
   *
   * @param array &$form
   *   Reference to the built structure of the entity field form being updated.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Current form state, user input and storage for the processed form.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse|array
   *   An AjaxResponse populated with the commands to execute on the
   *   user client.
   */
  public static function ajaxFieldRemoval(array &$form, FormStateInterface $form_state): array|AjaxResponse {
    $trigger = $form_state->getTriggeringElement();
    $parents = array_slice($trigger['#array_parents'], 0, -2);
    $element = &NestedArray::getValue($form, $parents);

    return $element;
  }

}

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

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