inline_media_form-1.0.0-beta1/src/Plugin/Field/FieldWidget/InlineMediaFormWidget.php

src/Plugin/Field/FieldWidget/InlineMediaFormWidget.php
<?php

namespace Drupal\inline_media_form\Plugin\Field\FieldWidget;

use Drupal\Component\Datetime\Time;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AnnounceCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Field\WidgetInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\TypedData\TranslationStatusInterface;
use Drupal\field_group\FormatterHelper;
use Drupal\inline_media_form\MediaItemAdapter;
use Drupal\inline_media_form\MediaItemAdapterInterface;
use Drupal\media\Entity\Media;
use Drupal\media\MediaInterface;
use Drupal\media_library\MediaLibraryState;
use Drupal\media_library\MediaLibraryUiBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;

/**
 * Custom entity browser and edit widget for media file Entity Reference fields.
 *
 * @FieldWidget(
 *   id = "inline_media_form",
 *   label = @Translation("Inline media form"),
 *   description = @Translation("Uses entity browser to select entities, and then provides a way to edit selected media without leaving the entity form."),
 *   field_types = {
 *     "entity_reference"
 *   }
 * )
 */
class InlineMediaFormWidget extends WidgetBase {

  // ===========================================================================
  // Constants
  // ===========================================================================

  /**
   * Action position is in the add media files place.
   */
  const ACTION_POSITION_BASE = 1;

  /**
   * Action position is in the table header section.
   */
  const ACTION_POSITION_HEADER = 2;

  /**
   * Action position is in the toolbar sub-component of table header section.
   */
  const ACTION_POSITION_HEADER_TOOLBAR = 3;

  /**
   * Action position is a single media file row in the widget.
   */
  const ACTION_POSITION_ITEM_ROW = 4;

  /**
   * Edit more for a widget that is open for editing.
   */
  const EDIT_MODE_OPEN = 'open';

  /**
   * Edit more for a widget that is closed and not actively editable.
   */
  const EDIT_MODE_CLOSED = 'closed';

  /**
   * Edit more for a widget that has been removed/is hidden from view.
   */
  const EDIT_MODE_REMOVED = 'removed';

  /**
   * Option value used when auto-collapse is disabled.
   */
  const AUTO_COLLAPSE_DISABLED = 'no';

  /**
   * Option value used when auto-collapse is enabled.
   */
  const AUTO_COLLAPSE_ENABLED = 'yes';

  /**
   * Feature for when "Collapse all" and "Enable all" are available.
   */
  const FEATURE_COLLAPSE_EDIT_ALL = 'collapse_edit_all';

  // ===========================================================================
  // Member Fields
  // ===========================================================================

  /**
   * Indicates whether the current widget instance is in translation.
   *
   * @var bool
   */
  protected $isTranslating;

  /**
   * ID to name ajax buttons that includes field parents and field name.
   *
   * @var string
   */
  protected $fieldIdPrefix;

  /**
   * Wrapper id to identify the media files.
   *
   * @var string
   */
  protected $fieldWrapperId;

  /**
   * Number of media files item on form.
   *
   * @var int
   */
  protected $realItemCount;

  /**
   * Parents for the current media file.
   *
   * @var array
   */
  protected $fieldParents;

  /**
   * The Drupal module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

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

  /**
   * Information on the different bundles of each entity type.
   *
   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
   */
  protected $entityBundleInfo;

  /**
   * The repository of entity display information.
   *
   * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
   */
  protected $entityDisplayRepository;

  /**
   * The entity reference selection plugin manager.
   *
   * This manages plug-ins that provide users with a way to select which
   * entity or entities should be the target of an entity reference field.
   *
   * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface
   */
  protected $selectionManager;

  /**
   * The content translation management interface.
   *
   * @var \Drupal\content_translation\ContentTranslationManagerInterface
   */
  protected $translationManager;

  /**
   * The logged-in user.
   *
   * @var \Drupal\Core\Session\AccountProxy
   */
  protected $user;

  /**
   * The time service.
   *
   * @var \Drupal\Component\Datetime\Time
   */
  protected $time;

  // ===========================================================================
  // IoC Constructor
  // ===========================================================================

  /**
   * {@inheritDoc}
   */
  public static function create(ContainerInterface $container,
                                array $configuration,
                                $plugin_id,
                                $plugin_definition): WidgetInterface {
    /** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
    $module_handler = $container->get('module_handler');

    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
    $entity_type_manager = $container->get('entity_type.manager');

    /** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_bundle_info */
    $entity_bundle_info = $container->get('entity_type.bundle.info');

    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository */
    $entity_display_repository = $container->get('entity_display.repository');

    /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */
    $selection_manager =
      $container->get('plugin.manager.entity_reference_selection');

    /** @var \Drupal\content_translation\ContentTranslationManagerInterface $translation_manager */
    if ($module_handler->moduleExists('content_translation')) {
      $translation_manager = $container->get('content_translation.manager');
    }
    else {
      $translation_manager = NULL;
    }

    /** @var \Drupal\Core\Session\AccountProxyInterface $user */
    $user = $container->get('current_user');

    /** @var \Drupal\Component\Datetime\Time $time */
    $time = $container->get('datetime.time');

    return new static(
      $plugin_id,
      $plugin_definition,
      $configuration['field_definition'],
      $configuration['settings'],
      $configuration['third_party_settings'],
      $module_handler,
      $entity_type_manager,
      $entity_bundle_info,
      $entity_display_repository,
      $selection_manager,
      $translation_manager,
      $user,
      $time
    );
  }

  // ===========================================================================
  // Widget Callbacks
  // ===========================================================================

  /**
   * {@inheritdoc}
   */
  public static function isApplicable(
                             FieldDefinitionInterface $field_definition): bool {
    $target_type_id = $field_definition->getSetting('target_type');

    try {
      $target_type =
        \Drupal::entityTypeManager()->getDefinition($target_type_id);

      return $target_type->entityClassImplements(MediaInterface::class);
    }
    catch (PluginNotFoundException $ex) {
      watchdog_exception(
        'inline_media_form',
        $ex,
        'Failed to retrieve type definition for target entity type %entity_type - %type: @message in %function (line %line of %file).',
        ['%entity_type' => $target_type_id]
      );

      return FALSE;
    }
  }

  // ===========================================================================
  // AJAX Callbacks
  // ===========================================================================

  /**
   * Dispatches and renders the result of opening the media library modal.
   *
   * @param array $form
   *   The entity edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   An AJAX response to open the media library.
   *
   * @noinspection PhpUnusedParameterInspection
   */
  public static function handleOpenMediaLibraryAjax(
                                array $form,
                                FormStateInterface $form_state): AjaxResponse {
    $triggering_element = $form_state->getTriggeringElement();

    $opener_state = $triggering_element['#media_library_opener_state'];
    $library_state = self::buildMediaLibraryState(
      $opener_state,
      $form['#build_id'],
    );

    $library_ui =
      \Drupal::service('media_library.ui_builder')
        ->buildUi($library_state);

    $dialog_options = MediaLibraryUiBuilder::dialogOptions();
    $response       = new AjaxResponse();

    $response->addCommand(
      new OpenModalDialogCommand(
        $dialog_options['title'],
        $library_ui,
        $dialog_options
      )
    );

    return $response;
  }

  /**
   * AJAX callback to update the widget when additional, new media are selected.
   *
   * @param array $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   An AJAX response to update the selection.
   */
  public static function handleMediaSelectedAjax(
                                array $form,
                                FormStateInterface $form_state): AjaxResponse {
    $submit     = static::getSubmitElementInfo($form, $form_state);
    $wrapper_id = $submit['button']['#ajax']['wrapper'] ?? '';

    $element = $submit['element'];

    // Clear hidden textfield selection to reduce how often we have to filter
    // out duplicate IDs if the user re-opens the media browser.
    $element['add_more']['media_library_selection']['#value'] = '';

    $response = new AjaxResponse();

    $response->addCommand(new ReplaceCommand("#$wrapper_id", $element));

    $new_items = static::getNewMediaItems($element, $form_state);
    $new_count = count($new_items);

    $label        = $element['#reference_title'];
    $plural_label = $element['#reference_title_plural'];

    $announcement =
      \Drupal::translation()->formatPlural(
        $new_count,
        'Added one @title.',
        'Added @count @title_plural.',
        [
          '@title'        => $label,
          '@title_plural' => $plural_label,
        ]
      );

    $response->addCommand(new AnnounceCommand($announcement));

    return $response;
  }

  /**
   * Dispatches and renders the result of AJAX actions that affect all deltas.
   *
   * In other words, this returns an updated copy of the entire widget to the
   * browser.
   *
   * @param array $form
   *   The entity edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   *
   * @return array
   *   The form element to render back to the client.
   */
  public static function handleAllItemAjax(
                                        array $form,
                                        FormStateInterface $form_state): array {
    $submit =
      static::getSubmitElementInfo(
        $form,
        $form_state,
        static::ACTION_POSITION_HEADER
      );

    return $submit['element'];
  }

  /**
   * Dispatches and renders the result of toolbar actions like bulk rename.
   *
   * Returns an updated copy of the entire widget to the browser.
   *
   * @param array $form
   *   The entity edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   *
   * @return array
   *   The form element to render back to the client.
   */
  public static function handleToolbarAjax(
                                        array $form,
                                        FormStateInterface $form_state): array {
    $submit =
      static::getSubmitElementInfo(
        $form,
        $form_state,
        static::ACTION_POSITION_HEADER_TOOLBAR
      );

    return $submit['element'];
  }

  /**
   * Dispatches and renders the result of AJAX actions that affect one delta.
   *
   * @param array $form
   *   The entity edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   *
   * @return array
   *   The form element to render back to the client.
   */
  public static function handleSingleItemAjax(array $form,
                                        FormStateInterface $form_state): array {
    $submit =
      static::getSubmitElementInfo(
        $form,
        $form_state,
        static::ACTION_POSITION_ITEM_ROW
      );

    $element = $submit['element'];

    $element['#prefix'] =
      '<div class="ajax-new-content">' . ($element['#prefix'] ?? '');

    $element['#suffix'] =
      ($element['#suffix'] ?? '') . '</div>';

    return $element;
  }

  // ===========================================================================
  // Submit and Form Process Callbacks
  // ===========================================================================

  /**
   * Value callback for optimizing storage of media type weights.
   *
   * The tabledrag functionality needs a specific weight field, but we don't
   * want to store this extra weight field in our settings.
   *
   * Adapted from
   * \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::setMediaTypesValue().
   *
   * @param array $element
   *   An associative array containing the properties of the element.
   * @param array|bool $input
   *   Either the incoming input to populate the form element; or FALSE if the
   *   element's default value should be returned.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return mixed
   *   The value to assign to the element.
   */
  public static function storeMediaTabWeights(array &$element,
                                              $input,
                                              FormStateInterface $form_state) {
    $value = $element['#default_value'] ?? [];

    if (!empty($input)) {
      // Sort media types by 'weight' value.
      uasort($input, 'Drupal\Component\Utility\SortArray::sortByWeightElement');

      // Update form state with sorted values.
      $sorted_media_type_ids = array_keys($input);
      $form_state->setValue($element['#parents'], $sorted_media_type_ids);

      // Unset child elements containing weights for each media type to avoid
      // FormBuilder::doBuildForm() processing and storing the weight fields as
      // well.
      foreach ($sorted_media_type_ids as $media_type_id) {
        unset($element[$media_type_id]);
      }

      $value = $sorted_media_type_ids;
    }

    return $value;
  }

  /**
   * Validates that newly selected items can be added to the widget.
   *
   * Making an invalid selection from the view should not be possible, but we
   * still validate in case other selection methods (ex: upload) are valid.
   *
   * @param array $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public static function validateSelectedMediaItems(
                                        array $form,
                                        FormStateInterface $form_state): void {
    $submit = static::getSubmitElementInfo($form, $form_state);

    $widget_state = $submit['widget_state'];
    $element      = $submit['element'];

    $new_media = static::getNewMediaItems($element, $form_state);

    if (empty($new_media)) {
      return;
    }

    $cardinality    = $element['#cardinality'];
    $target_bundles = $element['#target_bundles'];

    $is_cardinality_unlimited =
      ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);

    $current_item_count = $widget_state['items_count'];
    $total_media        = $current_item_count + count($new_media);

    if (!$is_cardinality_unlimited && ($total_media > $cardinality)) {
      $form_state->setError(
        $element,
        \Drupal::translation()->formatPlural(
          $cardinality,
          'Only one media file can be selected.',
          'Only @count media files can be selected.'
        )
      );
    }

    if (!empty($target_bundles)) {
      $allowed_media_type_ids = array_keys($target_bundles);

      foreach ($new_media as $media_item) {
        if (!in_array($media_item->bundle(), $allowed_media_type_ids, TRUE)) {
          $label = $element['#reference_title'];

          $form_state->setError(
            $element,
            t('The new @title "@label" is not of an accepted type. Allowed types: @types',
              [
                '@title' => $label,
                '@label' => $media_item->label(),
                '@types' => implode(', ', array_values($target_bundles)),
              ]
            )
          );
        }
      }
    }
  }

  /**
   * Submit callback for adding selected media to the widget.
   *
   * @param array $form
   *   The entity edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   */
  public static function addSelectedMediaItemSubmit(
                                        array $form,
                                        FormStateInterface $form_state): void {
    $submit = static::getSubmitElementInfo($form, $form_state);

    $widget_state = $submit['widget_state'];
    $element      = $submit['element'];

    $new_media = static::getNewMediaItems($element, $form_state);

    if (empty($new_media)) {
      return;
    }

    $field_path =
      array_merge($element['#field_parents'], [$element['#field_name']]);

    $max_delta = $element['#max_delta'];
    $new_delta = $max_delta + 1;

    $new_deltas = [];

    foreach ($new_media as $media_entity) {
      // Any ID can be passed to the widget, so we have to check access.
      if (!$media_entity->access('view')) {
        continue;
      }

      static::prepareDeltaPosition(
        $widget_state,
        $form_state,
        $field_path,
        $new_delta
      );

      $wrapped_media = MediaItemAdapter::create($media_entity);

      $widget_delta_state            =& $widget_state['media'][$new_delta];
      $widget_delta_state['wrapper'] = $wrapped_media;
      $widget_delta_state['entity']  = $wrapped_media->toMediaEntity();

      $new_deltas[] = $new_delta;

      ++$new_delta;
    }

    $widget_state = static::applyAutoCollapse($widget_state);

    // Ensure all the new files start out open for editing after the
    // auto-collapse.
    foreach ($new_deltas as $new_delta) {
      $widget_state['media'][$new_delta]['mode'] = self::EDIT_MODE_OPEN;
    }

    static::setWidgetState(
      $submit['parents'],
      $submit['field_name'],
      $form_state,
      $widget_state
    );

    $form_state->setRebuild();
  }

  /**
   * Submit callback for performing an action on a single media file item.
   *
   * @param array $form
   *   The entity edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   */
  public static function mediaFileItemSubmit(
                                        array $form,
                                        FormStateInterface $form_state): void {
    $submit =
      static::getSubmitElementInfo(
        $form,
        $form_state,
        static::ACTION_POSITION_ITEM_ROW
      );

    $button       = $submit['button'];
    $widget_state = $submit['widget_state'];
    $delta        = $submit['delta'];

    $new_mode = $button['#edit_mode'];

    if ($new_mode === self::EDIT_MODE_OPEN) {
      $widget_state = static::applyAutoCollapse($widget_state);

      // Close/hide bulk rename toolbar.
      $widget_state['show_bulk_rename_options'] = FALSE;
    }

    $widget_delta_state         =& $widget_state['media'][$delta];
    $widget_delta_state['mode'] = $new_mode;

    static::setWidgetState(
      $submit['parents'],
      $submit['field_name'],
      $form_state,
      $widget_state
    );

    $form_state->setRebuild();
  }

  /**
   * Submit callback for toggling all media files into or out of an edit mode.
   *
   * @param array $form
   *   The entity edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   */
  public static function changeAllEditModeSubmit(
                                        array $form,
                                        FormStateInterface $form_state): void {
    $submit =
      static::getSubmitElementInfo(
        $form,
        $form_state,
        static::ACTION_POSITION_HEADER
      );

    // Activate edit mode (or deactivate if already activated)
    $widget_state = $submit['widget_state'];

    foreach ($widget_state['media'] as &$widget_delta_state) {
      if ($widget_delta_state['mode'] === self::EDIT_MODE_REMOVED) {
        // Skip deltas we've marked for deletion.
        continue;
      }

      $widget_delta_state['mode'] = $submit['button']['#edit_mode'];
    }

    // Close/hide bulk rename toolbar.
    $widget_state['show_bulk_rename_options'] = FALSE;

    static::setWidgetState(
      $submit['parents'],
      $submit['field_name'],
      $form_state,
      $widget_state
    );

    $form_state->setRebuild();
  }

  /**
   * Submit callback for the button that shows the bulk rename toolbar.
   *
   * @param array $form
   *   The entity edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   */
  public static function showBulkRenameToolbarSubmit(
                                        array $form,
                                        FormStateInterface $form_state): void {
    $submit =
      static::getSubmitElementInfo(
        $form,
        $form_state,
        static::ACTION_POSITION_HEADER
      );

    $widget_state = $submit['widget_state'];

    $widget_state['show_bulk_rename_options'] = TRUE;

    static::setWidgetState(
      $submit['parents'],
      $submit['field_name'],
      $form_state,
      $widget_state
    );

    $form_state->setRebuild();
  }

  /**
   * Submit callback for the button that hides the bulk rename toolbar.
   *
   * @param array $form
   *   The entity edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   */
  public static function hideBulkRenameToolbarSubmit(
                                        array $form,
                                        FormStateInterface $form_state): void {
    $submit =
      static::getSubmitElementInfo(
        $form,
        $form_state,
        static::ACTION_POSITION_HEADER_TOOLBAR
      );

    $widget_state = $submit['widget_state'];

    $widget_state['show_bulk_rename_options'] = FALSE;

    static::setWidgetState(
      $submit['parents'],
      $submit['field_name'],
      $form_state,
      $widget_state
    );

    $form_state->setRebuild();
  }

  /**
   * Submit callback for applying bulk renames to all media files.
   *
   * @param array $form
   *   The entity edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   */
  public static function bulkRenameAllMediaSubmit(
                                        array $form,
                                        FormStateInterface $form_state): void {
    $submit =
      static::getSubmitElementInfo(
        $form,
        $form_state,
        static::ACTION_POSITION_HEADER_TOOLBAR
      );

    $widget_state = $submit['widget_state'];
    $button       = $submit['button'];

    assert(isset($button['#parents']));

    // Get our parents relative to the "Rename" button that was clicked, since
    // it's part of the same toolbar.
    $toolbar_parents = array_slice($button['#parents'], 0, -1);

    $raw_input          = $form_state->getUserInput();
    $raw_toolbar_inputs = NestedArray::getValue($raw_input, $toolbar_parents);

    if (isset($raw_toolbar_inputs['name'])) {
      $raw_name      = $raw_toolbar_inputs['name'];
      $is_sequential = $raw_toolbar_inputs['number_sequentially'];

      // Ensure that we escape any HTML or unsafe markup. The trim ensures that
      // leading or trailing spaces in the name are not preserved.
      $trimmed_name = trim($raw_name);

      // Number of digits to pad up to (minimum length of 2, so 01, 02, etc.).
      $num_digits = max(2, (int) log10(count($widget_state['media'])) + 1);

      $file_index = 1;

      foreach ($widget_state['media'] as &$widget_delta_state) {
        $media_entity = $widget_delta_state['entity'];
        $item_mode    = $widget_delta_state['mode'] ?? NULL;

        if ($item_mode === self::EDIT_MODE_REMOVED) {
          continue;
        }

        if ($is_sequential) {
          // Add a padded counter to the end of each media file, renaming it to
          // be "Name 001", "Name 002", etc.
          $padded_index = str_pad($file_index, $num_digits, '0', STR_PAD_LEFT);

          // This trim ensures we don't end up with leading spaces in case the
          // name is blank. (Unlikely, but it could happen if requirements for
          // the form change).
          $new_name = ltrim(sprintf('%s %s', $trimmed_name, $padded_index));
        }
        else {
          // Rename each media file to be "Name", "Name", etc.
          $new_name = $trimmed_name;
        }

        $media_entity->set('name', $new_name);

        $file_index++;
      }
    }

    // Close the bulk rename widget, since it's no longer needed.
    $widget_state['show_bulk_rename_options'] = FALSE;

    static::setWidgetState(
      $submit['parents'],
      $submit['field_name'],
      $form_state,
      $widget_state
    );

    $form_state->setRebuild();
  }

  /**
   * After-build callback for adding the translatability clue from the widget.
   *
   * Adds a clue about the form element translatability. This replicates the
   * logic of ContentTranslationHandler::addTranslatabilityClue().
   *
   * If the given element does not have a #title attribute, the function is
   * recursively applied to child elements.
   *
   * @param array $element
   *   The form element to which the clue needs to be added.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The state of the form containing the element.
   *
   * @return array
   *   The form element with the translatability clue added.
   */
  public static function addTranslatabilityClue(
                                        array $element,
                                        FormStateInterface $form_state): array {
    static $suffix,
           $fapi_title_elements;

    // Widgets could have multiple elements with their own titles, so remove the
    // suffix if it exists, do not recurse lower than this to avoid going into
    // nested media or similar nested field types.
    // Elements which can have a #title attribute according to FAPI Reference.
    if (!isset($suffix)) {
      $suffix =
        sprintf(
          ' <span class="translation-entity-all-languages">(%s)</span>',
          t('all languages')
        );

      $fapi_title_elements = [
        'checkbox',
        'checkboxes',
        'date',
        'details',
        'fieldset',
        'file',
        'item',
        'password',
        'password_confirm',
        'radio',
        'radios',
        'select',
        'textarea',
        'textfield',
        'weight',
      ];
    }

    $element_type = $element['#type'] ?? NULL;

    // Update #title attribute for all elements that are allowed to have a
    // #title attribute according to the Form API Reference. The reason for this
    // check is because some elements have a #title attribute even though it is
    // not rendered; for instance, field containers.
    if (($element_type !== NULL) &&
        in_array($element_type, $fapi_title_elements) &&
        !empty($element['#title'])) {
      $element['#title'] .= $suffix;
    }
    // If the current element does not have a (valid) title, try child elements.
    elseif ($children = Element::children($element)) {
      foreach ($children as $delta) {
        $element[$delta] =
          static::addTranslatabilityClue($element[$delta], $form_state);
      }
    }
    // If there are no children, fall back to the current #title attribute if it
    // exists.
    elseif (isset($element['#title'])) {
      $element['#title'] .= $suffix;
    }

    return $element;
  }

  /**
   * Form process callback for adding submit callbacks to entity forms.
   *
   * This is necessary because entity forms don't respond to adding '#submit'
   * callbacks at the top level -- they have to be added to the submit button
   * itself. But, submit buttons/actions on entity forms are not part of the
   * form at the time the IMF widget is being constructed. So, we have to
   * register a process callback so we can act on the mostly complete form.
   *
   * @param array $form
   *   The render array for the entity edit form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The state of the edit form.
   *
   * @return array
   *   The modified form render array.
   *
   * @noinspection PhpUnusedParameterInspection
   */
  public static function entityFormProcess(
                                        array $form,
                                        FormStateInterface $form_state): array {
    $have_submit = FALSE;

    // Apply to all actions that save the node form, not just the save button.
    // This is necessary for compatibility with modules like "Create and
    // Continue", "Add Another", "Entity Save and Add Another", etc.
    foreach (Element::children($form['actions']) as $action_key) {
      $action = $form['actions'][$action_key];

      $action_type      = $action['#type'] ?? NULL;
      $submit_callbacks = $action['#submit'] ?? [];

      if (($action_type == 'submit') && in_array('::save', $submit_callbacks)) {
        array_unshift(
          $form['actions'][$action_key]['#submit'],
          [static::class, 'saveModifiedMedia']
        );

        // Sanity check for assertions.
        $have_submit = TRUE;
      }
    }

    assert($have_submit, 'This entity form is missing a submit button.');

    return $form;
  }

  /**
   * Saves all media files that have been modified by this widget.
   *
   * @param array $form
   *   The entity edit form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The state of the edit form.
   */
  public static function saveModifiedMedia(
                                        array $form,
                                        FormStateInterface $form_state): void {
    $imf_fields = $form_state->get('inline_media_form_fields');

    foreach ($imf_fields as $imf_field) {
      $widget_state =
        static::getWidgetState(
          $imf_field['parents'],
          $imf_field['field_name'],
          $form_state
        );
      assert(!empty($widget_state));

      foreach ($widget_state['media'] as $widget_delta_state) {
        $wrapped_media = $widget_delta_state['wrapper'];
        assert($wrapped_media instanceof MediaItemAdapterInterface);

        try {
          $wrapped_media->saveIfNecessary();
        }
        catch (EntityStorageException $ex) {
          $media_id = $wrapped_media->toMediaEntity()->id();

          $form_state->setError(
            $form,
            t('Unable to save modified media item %id: @error',
              [
                '%id'    => $media_id,
                '@error' => $ex->getMessage(),
              ]
            )
          );

          watchdog_exception(
            'inline_media_form',
            $ex,
            'Unable to save media item %id - %type: @message in %function (line %line of %file).',
            ['%id' => $media_id]
          );
        }
      }
    }
  }

  // ===========================================================================
  // Static Utility Methods
  // ===========================================================================

  /**
   * Gets newly selected media items.
   *
   * Adapted from MediaLibraryWidget::getNewMediaItems.
   *
   * @param array $element
   *   The wrapping element for this widget.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return \Drupal\media\MediaInterface[]
   *   An array of selected media items.
   *
   * @see \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::getNewMediaItems
   */
  protected static function getNewMediaItems(
                                        array $element,
                                        FormStateInterface $form_state): array {
    $new_items = [];

    // Get the new media IDs passed to our hidden button. We need to use the
    // actual user input, since when #limit_validation_errors is used, the
    // unvalidated user input is not added to the form state.
    // @see FormValidator::handleErrorsWithLimitedValidation()
    $values = $form_state->getUserInput();
    $path   = array_merge($element['#parents'], ['add_more']);
    $value  = NestedArray::getValue($values, $path);

    if (!empty($value['media_library_selection'])) {
      $ids = explode(',', $value['media_library_selection']);
      $ids = array_filter($ids, 'is_numeric');

      if (!empty($ids)) {
        /** @var \Drupal\media\MediaInterface[] $media */
        $new_items = Media::loadMultiple($ids);
      }
    }

    return $new_items;
  }

  /**
   * Extracts common submit element info during an AJAX submit callback.
   *
   * @param array $form
   *   The edit form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the edit form.
   * @param int $position
   *   The relative location within the form of the triggering element.
   *
   * @return array
   *   Submit element information.
   */
  public static function getSubmitElementInfo(
            array $form,
            FormStateInterface $form_state,
            $position = InlineMediaFormWidget::ACTION_POSITION_BASE): array {
    $submit = [
      'button' => $form_state->getTriggeringElement(),
    ];

    // Go up in the form, to the widgets container.
    $submit_parents = $submit['button']['#array_parents'];

    if ($position === static::ACTION_POSITION_BASE) {
      $element =
        NestedArray::getValue($form, array_slice($submit_parents, 0, -2));
    }
    elseif ($position === static::ACTION_POSITION_HEADER) {
      $element =
        NestedArray::getValue($form, array_slice($submit_parents, 0, -3));
    }
    elseif ($position === static::ACTION_POSITION_HEADER_TOOLBAR) {
      $element =
        NestedArray::getValue($form, array_slice($submit_parents, 0, -4));
    }
    elseif ($position === static::ACTION_POSITION_ITEM_ROW) {
      $element =
        NestedArray::getValue($form, array_slice($submit_parents, 0, -5));

      $delta = array_slice($submit_parents, -5, -4);

      $submit['delta'] = $delta[0];
    }
    else {
      throw new \InvalidArgumentException("Invalid position: ${position}");
    }

    $submit['element']    = $element;
    $submit['field_name'] = $element['#field_name'];
    $submit['parents']    = $element['#field_parents'];

    // Get widget state.
    $submit['widget_state'] =
      static::getWidgetState(
        $submit['parents'],
        $submit['field_name'],
        $form_state
      );
    assert(!empty($submit['widget_state']));

    return $submit;
  }

  /**
   * Automatically collapses any open media files, if auto-collapse is enabled.
   *
   * @param array $widget_state
   *   The current widget state.
   *
   * @return array
   *   The altered widget state, with appropriate media files collapsed.
   */
  public static function applyAutoCollapse(array $widget_state): array {
    $real_item_count         = $widget_state['real_item_count'];
    $auto_collapse           = $widget_state['auto_collapse'];
    $auto_collapse_threshold = $widget_state['auto_collapse_threshold'];

    $auto_collapse_enabled = ($auto_collapse !== self::AUTO_COLLAPSE_DISABLED);

    if ($auto_collapse_enabled &&
        ($real_item_count > $auto_collapse_threshold)) {
      foreach ($widget_state['media'] as &$widget_delta_state) {
        $mode = $widget_delta_state['mode'] ?? NULL;

        if ($mode === self::EDIT_MODE_OPEN) {
          $widget_delta_state['mode'] = self::EDIT_MODE_CLOSED;
        }
      }
    }

    return $widget_state;
  }

  // ===========================================================================
  // Constructor
  // ===========================================================================

  /**
   * Constructs a MediaFileSelectorWidget object.
   *
   * @param string $plugin_id
   *   The plugin_id for the widget.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   The definition of the field to which the widget is associated.
   * @param array $settings
   *   The widget settings.
   * @param array $third_party_settings
   *   Any third party settings.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The Drupal module handler.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The manager of all entity type information.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_bundle_info
   *   Information on the different bundles of each entity type.
   * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
   *   The repository of entity display information.
   * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager
   *   The manager of plug-ins that provide the user with a way to select which
   *   entities should be the target of an entity reference field.
   * @param \Drupal\content_translation\ContentTranslationManagerInterface|null $translation_manager
   *   The content translation management interface; or, NULL if translation
   *   support is not enabled on the site.
   * @param \Drupal\Core\Session\AccountProxyInterface $user
   *   The logged-in user.
   * @param \Drupal\Component\Datetime\Time $time
   *   The time service.
   */
  public function __construct(
                    string $plugin_id,
                    $plugin_definition,
                    FieldDefinitionInterface $field_definition,
                    array $settings,
                    array $third_party_settings,
                    ModuleHandlerInterface $module_handler,
                    EntityTypeManagerInterface $entity_type_manager,
                    EntityTypeBundleInfoInterface $entity_bundle_info,
                    EntityDisplayRepositoryInterface $entity_display_repository,
                    SelectionPluginManagerInterface $selection_manager,
                    ?ContentTranslationManagerInterface $translation_manager,
                    AccountProxyInterface $user,
                    Time $time) {
    parent::__construct(
      $plugin_id,
      $plugin_definition,
      $field_definition,
      $settings,
      $third_party_settings
    );

    $this->moduleHandler           = $module_handler;
    $this->entityTypeManager       = $entity_type_manager;
    $this->entityBundleInfo        = $entity_bundle_info;
    $this->entityDisplayRepository = $entity_display_repository;
    $this->selectionManager        = $selection_manager;
    $this->translationManager      = $translation_manager;
    $this->user                    = $user;
    $this->time                    = $time;
  }

  // ===========================================================================
  // Public Methods
  // ===========================================================================

  /**
   * {@inheritdoc}
   */
  public static function defaultSettings(): array {
    return [
      'title'                   => t('media file'),
      'title_plural'            => t('media files'),
      'initial_mode'            => self::EDIT_MODE_CLOSED,
      'auto_collapse'           => self::AUTO_COLLAPSE_DISABLED,
      'auto_collapse_threshold' => 0,
      'preview_display_mode'    => 'inline_media_form',
      'form_display_mode'       => 'inline_media_form',
      'library_view_display'    => 'media_library.widget',
      'library_tab_order'       => [],
      'features'                => [
        self::FEATURE_COLLAPSE_EDIT_ALL => self::FEATURE_COLLAPSE_EDIT_ALL,
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function settingsForm(array $form,
                               FormStateInterface $form_state): array {
    $field_name = $this->fieldDefinition->getName();
    $elements   = [];

    $elements['title'] = [
      '#type'          => 'textfield',
      '#title'         => $this->t('Title text'),
      '#description'   => $this->t('Label to appear as title on the button as "Add new [title]". This label is translatable.'),
      '#default_value' => $this->getSetting('title'),
      '#required'      => TRUE,
    ];

    $elements['title_plural'] = [
      '#type'          => 'textfield',
      '#title'         => $this->t('Plural title text'),
      '#description'   => $this->t('Label in its plural form.'),
      '#default_value' => $this->getSetting('title_plural'),
      '#required'      => TRUE,
    ];

    $elements['initial_mode'] = [
      '#type'          => 'select',
      '#title'         => $this->t('Initial edit mode'),
      '#description'   => $this->t('How each media file initially appears when the edit form is first opened.'),
      '#options'       => $this->getSettingOptions('initial_mode'),
      '#default_value' => $this->getSetting('initial_mode'),
      '#required'      => TRUE,
    ];

    $edit_mode_selector =
      'select[name="fields[' . $field_name . '][settings_edit_form][settings][edit_mode]"]';

    $elements['auto_collapse'] = [
      '#type'          => 'select',
      '#title'         => $this->t('Auto-collapse media files'),
      '#description'   => $this->t('If a media file opened for editing should be collapsed when starting to edit a different media file.'),
      '#options'       => $this->getSettingOptions('auto_collapse'),
      '#default_value' => $this->getSetting('auto_collapse'),
      '#required'      => TRUE,
      '#states'        => [
        'visible' => [
          $edit_mode_selector => ['value' => self::EDIT_MODE_CLOSED],
        ],
      ],
    ];

    $elements['auto_collapse_threshold'] = [
      '#type'          => 'number',
      '#title'         => $this->t('Threshold for auto-collapse'),
      '#default_value' => $this->getSetting('auto_collapse_threshold'),
      '#description'   => $this->t('Number of items (deltas) that must be present before media files start getting automatically collapsed. For example, if the threshold is 3, and there are only 2 or fewer media files, all media files will remain open when jumping between editing different media files.'),
      '#min'           => 0,
      '#states'        => [
        'invisible' => [
          $edit_mode_selector => ['value' => self::EDIT_MODE_OPEN],
        ],
      ],
    ];

    $target_type_id = $this->getFieldSetting('target_type');

    $view_display_mode_options =
      $this->entityDisplayRepository->getViewModeOptions($target_type_id);

    $elements['preview_display_mode'] = [
      '#type'          => 'select',
      '#options'       => $view_display_mode_options,
      '#title'         => $this->t('Preview display mode'),
      '#description'   => $this->t('The display mode to use when displaying a preview of each media file next to its summary.'),
      '#default_value' => $this->getSetting('preview_display_mode'),
      '#required'      => TRUE,
    ];

    $form_display_mode_options =
      $this->entityDisplayRepository->getFormModeOptions($target_type_id);

    $elements['form_display_mode'] = [
      '#type'          => 'select',
      '#options'       => $form_display_mode_options,
      '#title'         => $this->t('Form display mode'),
      '#description'   => $this->t('The form display mode to use when rendering the media file form.'),
      '#default_value' => $this->getSetting('form_display_mode'),
      '#required'      => TRUE,
    ];

    if (self::doesMediaLibrarySupportCustomViewDisplay()) {
      $view_displays = $this->getMediaLibraryViewDisplays();

      $elements['library_view_display'] = [
        '#type'          => 'select',
        '#options'       => $view_displays,
        '#title'         => $this->t('Media library view display'),
        '#description'   => $this->t('The view and display mode to use when displaying listings in the media library.'),
        '#default_value' => $this->getSetting('library_view_display'),
        '#required'      => TRUE,
      ];
    }

    $elements['features'] = [
      '#type'          => 'checkboxes',
      '#title'         => $this->t('Enable widget features'),
      '#options'       => $this->getSettingOptions('features'),
      '#default_value' => $this->getSetting('features'),
      '#description'   => $this->t('The features available to users during editing.'),
      '#multiple'      => TRUE,
    ];

    $allowed_types = $this->getAllowedTypes();

    if (count($allowed_types) > 1) {
      $library_tab_order = $this->buildMediaTabSortWidget($allowed_types);

      $elements['library_tab_order'] = $library_tab_order;
    }

    return $elements;
  }

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

    $summary[] =
      $this->t('Title: @title', ['@title' => $this->getSetting('title')]);

    $summary[] =
      $this->t(
        'Plural title: @title_plural',
        ['@title_plural' => $this->getSetting('title_plural')]
      );

    $edit_mode_options = $this->getSettingOptions('initial_mode');
    $initial_mode      = $this->getSetting('initial_mode');
    $edit_mode_label   = $edit_mode_options[$initial_mode];

    $summary[] =
      $this->t(
        'Initial edit mode: @edit_mode',
        ['@edit_mode' => $edit_mode_label]
      );

    if ($initial_mode == self::EDIT_MODE_CLOSED) {
      $auto_collapse_options = $this->getSettingOptions('auto_collapse');
      $auto_collapse         = $this->getSetting('auto_collapse');
      $auto_collapse_label   = $auto_collapse_options[$auto_collapse];

      $summary[] =
        $this->t(
          'Auto-collapse: @auto_collapse',
          ['@auto_collapse' => $auto_collapse_label]
        );
    }

    $auto_collapse_threshold = $this->getSetting('auto_collapse_threshold');

    if (($initial_mode == self::EDIT_MODE_CLOSED) &&
        ($auto_collapse_threshold > 0)) {
      $summary[] =
        $this->t(
          'Auto-collapse threshold: @mode_limit',
          ['@mode_limit' => $auto_collapse_threshold]
        );
    }

    $summary[] =
      $this->t(
        'Preview display mode: @preview_display_mode',
        ['@preview_display_mode' => $this->getSetting('preview_display_mode')]
      );

    $summary[] =
      $this->t(
        'Form display mode: @form_display_mode',
        ['@form_display_mode' => $this->getSetting('form_display_mode')]
      );

    if (self::doesMediaLibrarySupportCustomViewDisplay()) {
      $summary[] =
        $this->t(
          'Media library view display: @library_view_display',
          ['@library_view_display' => $this->getSetting('library_view_display')]
        );
    }

    $features_options = $this->getSettingOptions('features');
    $features_enabled = $this->getSetting('features');

    $features_enabled_labels =
      array_intersect_key($features_options, array_filter($features_enabled));

    if (!empty($features_enabled_labels)) {
      $summary[] =
        $this->t(
          'Features: @features',
          ['@features' => implode(', ', $features_enabled_labels)]
        );
    }

    $allowed_media_types = $this->getAllowedTypes();

    if (count($allowed_media_types) > 1) {
      $media_type_labels = [];

      foreach ($allowed_media_types as $media_info) {
        $media_type_labels[] = $media_info['label'];
      }

      $order_string = implode(', ', $media_type_labels);

      $summary[] =
        t('Media library tab order: @order', ['@order' => $order_string]);
    }

    return $summary;
  }

  /**
   * {@inheritdoc}
   *
   * Adds dependencies on display modes and views referenced by this widget.
   */
  public function calculateDependencies() {
    $dependencies = parent::calculateDependencies();

    $display_mode_settings = [
      'preview_display_mode',
      'form_display_mode',
    ];

    if (self::doesMediaLibrarySupportCustomViewDisplay()) {
      $view_display_settings = ['library_view_display'];
    }
    else {
      $view_display_settings = [];
    }

    $display_modes =
      array_unique(
        array_map(
          function ($setting_name) {
            return $this->getSetting($setting_name);
          },
          $display_mode_settings
        )
      );

    $view_ids =
      array_unique(
        array_map(
          function ($setting_name) {
            $view_display_name = $this->getSetting($setting_name);

            [$view_id] = explode('.', $view_display_name);

            return $view_id;
          },
          $view_display_settings
        )
      );

    $display_storage = $this->getEntityViewModeStorage();

    foreach ($display_modes as $display_mode_name) {
      /** @var \Drupal\Core\Entity\Entity\EntityViewMode $view_mode */
      $view_mode = $display_storage->load('media.' . $display_mode_name);

      if (!empty($view_mode)) {
        $dependency_key  = $view_mode->getConfigDependencyKey();
        $dependency_name = $view_mode->getConfigDependencyName();

        $dependencies =
          array_merge_recursive(
            $dependencies,
            [$dependency_key => [$dependency_name]]
          );
      }
    }

    $view_storage = $this->entityTypeManager->getStorage('view');

    foreach ($view_ids as $view_id) {
      $view = $view_storage->load($view_id);

      if (!empty($view)) {
        $dependency_key  = $view->getConfigDependencyKey();
        $dependency_name = $view->getConfigDependencyName();

        $dependencies =
          array_merge_recursive(
            $dependencies,
            [$dependency_key => [$dependency_name]]
          );
      }
    }

    return $dependencies;
  }

  /**
   * {@inheritdoc}
   */
  public function form(FieldItemListInterface $items,
                       array &$form,
                       FormStateInterface $form_state,
                       $get_delta = NULL): array {
    $parents = $form['#parents'];

    // Identify the manage field settings default value form.
    if (in_array('default_value_input', $parents, TRUE)) {
      // Since the entity is neither reusable nor cloneable, having a default
      // value is not supported.
      $elements = [
        '#markup' => $this->t(
          'No widget available for: %label.',
          ['%label' => $items->getFieldDefinition()->getLabel()]
        ),
      ];
    }
    else {
      $field_name = $this->getFieldName();
      $parents    = $form['#parents'];
      $field_path = array_merge($parents, [$field_name]);
      $id_prefix  = implode('-', $field_path);
      $wrapper_id = Html::getId($id_prefix . '-wrapper');

      $this->fieldParents   = $parents;
      $this->fieldIdPrefix  = $id_prefix;
      $this->fieldWrapperId = $wrapper_id;

      $imf_fields = $form_state->get('inline_media_form_fields') ?? [];

      $imf_fields[] = [
        'parents'    => $parents,
        'field_name' => $field_name,
      ];

      $form_state->set('inline_media_form_fields', $imf_fields);

      $form['#process'][] = [static::class, 'entityFormProcess'];

      $elements = parent::form($items, $form, $form_state, $get_delta);

      // Signal to content_translation that this field should be treated as
      // multilingual and not be hidden, see
      // \Drupal\content_translation\ContentTranslationHandler::entityFormSharedElements().
      $elements['#multilingual'] = TRUE;
    }

    return $elements;
  }

  /**
   * {@inheritdoc}
   */
  public function formMultipleElements(FieldItemListInterface $items,
                                       array &$form,
                                       FormStateInterface $form_state): array {
    $widget_state = $this->getCurrentWidgetState($form_state);
    $widget_state['ajax_wrapper_id'] = $this->fieldWrapperId;

    $max                 = $widget_state['items_count'];
    $this->realItemCount = $max;

    $field_name     = $this->fieldDefinition->getName();
    $field_prefix   = strtr($this->fieldIdPrefix, '_', '-');
    $entity_type_id = $items->getEntity()->getEntityTypeId();

    if (count($this->fieldParents) != 0) {
      if ($entity_type_id === 'media') {
        $form['#attributes']['class'][] = 'inline-media-form-nested';
      }
    }

    $elements = [];

    $elements['#prefix'] =
      sprintf(
        '<div class="inline-media-form-wrapper" id="%s">',
        $this->fieldWrapperId,
      );

    $elements['#suffix'] = '</div>';

    // Persist the widget state so formElement() can access it.
    static::setWidgetState(
      $this->fieldParents,
      $field_name,
      $form_state,
      $widget_state
    );

    $elements =
      $this->buildWidgetForEditMode(
        $form,
        $form_state,
        $items,
        $elements
      );

    return $elements;
  }

  /**
   * {@inheritdoc}
   *
   * @see \Drupal\content_translation\Controller\ContentTranslationController::prepareTranslation()
   *   Uses a similar approach to populate a new translation.
   *
   * @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
   */
  public function formElement(FieldItemListInterface $items,
                              $delta,
                              array $element,
                              array &$form,
                              FormStateInterface $form_state): array {
    $parents      = $element['#field_parents'];
    $widget_state = $this->getCurrentWidgetState($form_state, $parents);

    $this->initializeWidgetStateForDelta($items, $delta, $widget_state);

    /** @var \Drupal\Core\Entity\ContentEntityInterface $host_entity */
    $host_entity = $items->getEntity();
    assert($host_entity instanceof ContentEntityInterface);

    $field_name         = $this->fieldDefinition->getName();
    $widget_delta_state =& $widget_state['media'][$delta];

    $this->applyTranslation(
      $host_entity,
      $form_state,
      $widget_delta_state
    );

    $wrapped_media = $widget_delta_state['wrapper'] ?? NULL;
    assert(
      ($wrapped_media === NULL) ||
      ($wrapped_media instanceof MediaItemAdapterInterface)
    );

    $can_view_entity = $this->canView($wrapped_media);

    if (!$can_view_entity || ($wrapped_media === NULL)) {
      $element['#access'] = FALSE;

      $widget_state['inaccessible_item_count'] =
        ($widget_state['inaccessible_item_count'] ?? 0) + 1;

      unset($widget_state['media'][$delta]);
    }
    else {
      $element =
        $this->buildSingleWidget(
          $form,
          $form_state,
          $field_name,
          $widget_state,
          $items,
          $element,
          $delta,
          $wrapped_media
        );
    }

    static::setWidgetState($parents, $field_name, $form_state, $widget_state);

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function extractFormValues(FieldItemListInterface $items,
                                    array $form,
                                    FormStateInterface $form_state): void {
    // Filter possible empty items.
    $items->filterEmptyItems();

    // Remove buttons from header actions.
    $field_name           = $this->fieldDefinition->getName();
    $path                 = array_merge($form['#parents'], [$field_name]);
    $form_state_variables = $form_state->getValues();

    $key_exists = NULL;

    $values =
      NestedArray::getValue($form_state_variables, $path, $key_exists);

    if ($key_exists) {
      unset($values['header_actions']);

      NestedArray::setValue($form_state_variables, $path, $values);
      $form_state->setValues($form_state_variables);
    }

    parent::extractFormValues($items, $form, $form_state);
  }

  /**
   * Validates a single media file delta.
   *
   * @param array $element
   *   The form element (the widget) being validated.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The state of the edit form.
   */
  public function elementValidate(array $element,
                                  FormStateInterface $form_state): void {
    $parents      = $element['#field_parents'];
    $widget_state = $this->getCurrentWidgetState($form_state, $parents);

    $delta              = $element['#delta'];
    $widget_delta_state = $widget_state['media'][$delta] ?? NULL;

    if ($widget_delta_state != NULL) {
      $wrapped_media = $widget_delta_state['wrapper'];
      assert($wrapped_media instanceof MediaItemAdapterInterface);

      /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
      $display = $widget_delta_state['display'];

      if ($widget_delta_state['mode'] == self::EDIT_MODE_OPEN) {
        $media_entity = $wrapped_media->toMediaEntity();

        // Extract the form values on submit for getting the current media file.
        $display->extractFormValues(
          $media_entity,
          $element['subform'],
          $form_state
        );
      }
    }

    $field_name = $this->fieldDefinition->getName();

    static::setWidgetState($parents, $field_name, $form_state, $widget_state);
  }

  /**
   * Assembles an action button for a media file widget by adding boilerplate.
   *
   * @param array $base_element
   *   Partial render array for the button.
   *
   * @return array
   *   Fully-populated render array for the button.
   */
  public function buildActionButton(array $base_element): array {
    // Do not expand elements that do not have submit handler.
    if (empty($base_element['#submit'])) {
      return $base_element;
    }

    $button = $base_element + [
      '#type'           => 'submit',
      '#theme_wrappers' => ['input__submit__media_file_action'],
    ];

    // Html::getId will give us '-' char in name but we want '_' for now so
    // we use strtr to search & replace '-' to '_'.
    $button['#name'] = strtr(Html::getId($base_element['#name']), '-', '_');
    $button['#id']   = Html::getUniqueId($button['#name']);

    if (isset($button['#ajax'])) {
      $button['#ajax'] += [
        'effect' => 'fade',

        // Since a normal throbber is added inline, this has the potential to
        // break a layout if the button is located in dropbuttons. Instead,
        // it's safer to just show the fullscreen progress element instead.
        'progress' => ['type' => 'fullscreen'],
      ];
    }

    return $button;
  }

  /**
   * {@inheritdoc}
   */
  public function flagErrors(FieldItemListInterface $items,
                             ConstraintViolationListInterface $violations,
                             array $form,
                             FormStateInterface $form_state): void {
    $parents      = $form['#parents'];
    $widget_state = $this->getCurrentWidgetState($form_state, $parents);

    parent::flagErrors($items, $violations, $form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function errorElement(array $element,
                               ConstraintViolationInterface $error,
                               array $form,
                               FormStateInterface $form_state): array {
    // Validation errors might be a about a specific form element;
    // attempt to find a matching element.
    if (empty($error->arrayPropertyPath)) {
      $error_element = $element;
    }
    else {
      $error_element =
        NestedArray::getValue($element, $error->arrayPropertyPath) ?? $element;
    }

    return $error_element;
  }

  /**
   * Special handling to validate form elements with multiple values.
   *
   * @param array $elements
   *   The form elements (widget) being validated.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The state of the edit form.
   */
  public function multipleElementValidate(
                                        array $elements,
                                        FormStateInterface $form_state): void {
    $parents      = $elements['#field_parents'];
    $field_name   = $this->fieldDefinition->getName();
    $widget_state = $this->getCurrentWidgetState($form_state, $parents);

    if ($elements['#required'] && ($widget_state['real_item_count'] < 1)) {
      $form_state->setError(
        $elements,
        t('@name field is required.',
          ['@name' => $this->fieldDefinition->getLabel()])
      );
    }

    static::setWidgetState($parents, $field_name, $form_state, $widget_state);
  }

  /**
   * {@inheritdoc}
   */
  public function massageFormValues(array $values,
                                    array $form,
                                    FormStateInterface $form_state): array {
    $parents      = $form['#parents'];
    $widget_state = $this->getCurrentWidgetState($form_state, $parents);

    $element =
      NestedArray::getValue(
        $form_state->getCompleteForm(),
        $widget_state['array_parents']
      );

    foreach ($values as $delta => &$item) {
      $original_delta     = $item['_original_delta'];
      $widget_delta_state =& $widget_state['media'][$original_delta];

      $edit_mode     = $widget_delta_state['mode'] ?? NULL;
      $wrapped_media = $widget_delta_state['wrapper'] ?? NULL;
      $display       = $widget_delta_state['display'];

      if ($wrapped_media !== NULL) {
        $delta_element = $element[$original_delta];

        $item =
          $this->massageDeltaFormValues(
            $display,
            $form_state,
            $delta_element,
            $wrapped_media,
            $item,
            $edit_mode
          );
      }
    }

    return $values;
  }

  // ===========================================================================
  // Protected Methods
  // ===========================================================================

  /**
   * Gets the machine name of the field this widget is editing.
   *
   * @return string
   *   The machine name of the field.
   */
  protected function getFieldName(): string {
    return $this->fieldDefinition->getName();
  }

  /**
   * Gets whether translation support is enabled and available.
   *
   * This determination is based on whether the translation manager has been
   * injected at construction time.
   *
   * @return bool
   *   TRUE if translation support is enabled on the site.
   */
  protected function hasTranslationManager(): bool {
    return ($this->translationManager !== NULL);
  }

  /**
   * Checks if it is safe to re-order, add, or remove media files.
   *
   * @return bool
   *   TRUE if we can allow reference changes; or, FALSE, otherwise.
   */
  protected function allowReferenceChanges(): bool {
    return !$this->isTranslating;
  }

  /**
   * Returns what media file types the field can reference, sorted by weight.
   *
   * @param \Drupal\Core\Field\FieldDefinitionInterface|null $field_definition
   *   (optional) The field definition for which the allowed types should be
   *   returned, defaults to the current field.
   *
   * @return array
   *   A list of arrays keyed by the media file type machine name with the
   *   following properties.
   *     - label: The label of the media file type.
   *     - weight: The weight of the media file type.
   */
  protected function getAllowedTypes(
                     FieldDefinitionInterface $field_definition = NULL): array {
    $result = [];

    if ($field_definition === NULL) {
      $field_definition = $this->fieldDefinition;
    }
    assert(!empty($field_definition));

    $target_type_id = $field_definition->getSetting('target_type');
    $bundles        = $this->entityBundleInfo->getBundleInfo($target_type_id);

    $target_bundles = $this->getSelectionHandlerSetting('target_bundles');
    $sorted_bundles = $this->getSetting('library_tab_order');

    $bundle_weights  = array_flip($sorted_bundles);
    $sequence_weight = 0;

    foreach ($bundles as $machine_name => $bundle) {
      if (empty($target_bundles) || in_array($machine_name, $target_bundles)) {
        // Fallback to sequence order in case new media types were added since
        // the tab order was configured.
        $weight = $bundle_weights[$machine_name] ?? $sequence_weight;

        $result[$machine_name] = [
          'label'  => $bundle['label'],
          'weight' => $weight,
        ];

        ++$sequence_weight;
      }
    }

    // Re-sort the target types by their weights.
    uasort($result, 'Drupal\Component\Utility\SortArray::sortByWeightElement');

    return $result;
  }

  /**
   * Gets the storage interface for entity view modes.
   *
   * @return \Drupal\Core\Entity\EntityStorageInterface
   *   The view mode storage service.
   */
  protected function getEntityViewModeStorage(): EntityStorageInterface {
    try {
      return $this->entityTypeManager->getStorage('entity_view_mode');
    }
    catch (InvalidPluginDefinitionException | PluginNotFoundException $ex) {
      throw new \RuntimeException(
        'Failed to load entity view mode storage: ' . $ex->getMessage(),
        0,
        $ex
      );
    }
  }

  /**
   * Gets the storage interface for views.
   *
   * @return \Drupal\Core\Entity\EntityStorageInterface
   *   The view storage service.
   */
  protected function getViewStorage(): EntityStorageInterface {
    try {
      return $this->entityTypeManager->getStorage('view');
    }
    catch (InvalidPluginDefinitionException | PluginNotFoundException $ex) {
      throw new \RuntimeException(
        'Failed to load view storage: ' . $ex->getMessage(),
        0,
        $ex
      );
    }
  }

  /**
   * Returns select options for a plugin setting.
   *
   * @param string $setting_name
   *   The name of the widget setting. Supported settings:
   *   - "edit_mode": Options for how each media file is displayed in the edit
   *     form by default.
   *   - "auto_collapse": Options for whether a media file opened for editing
   *     should be collapsed when starting to edit a different media file.
   *
   * @return array|null
   *   An array of setting option usable as a value for a "#options" key.
   */
  protected function getSettingOptions(string $setting_name): ?array {
    switch ($setting_name) {
      case 'initial_mode':
        $options = [
          self::EDIT_MODE_OPEN   => $this->t('Open'),
          self::EDIT_MODE_CLOSED => $this->t('Closed'),
        ];
        break;

      case 'auto_collapse':
        $options = [
          self::AUTO_COLLAPSE_DISABLED => $this->t('No'),
          self::AUTO_COLLAPSE_ENABLED  => $this->t('Yes'),
        ];
        break;

      case 'features':
        $options = [
          self::FEATURE_COLLAPSE_EDIT_ALL => $this->t('Collapse / edit all'),
        ];
        break;
    }

    return ($options ?? NULL);
  }

  /**
   * Returns the value of a setting for the entity reference selection handler.
   *
   * @param string $setting_name
   *   The setting name.
   *
   * @return mixed
   *   The setting value.
   */
  protected function getSelectionHandlerSetting(string $setting_name) {
    $settings = $this->getFieldSetting('handler_settings');

    return ($settings[$setting_name] ?? NULL);
  }

  /**
   * Checks if a widget feature is enabled or not.
   *
   * @param string $feature
   *   Feature name to check.
   *
   * @return bool
   *   TRUE if the feature is enabled, otherwise FALSE.
   */
  protected function isFeatureEnabled(string $feature): bool {
    $features = $this->getSetting('features');

    return !empty($features[$feature]);
  }

  /**
   * Gets a list of all view displays that can be used for the media browser.
   *
   * Adapted from
   * \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::settingsForm()
   * introduced in #2971209.
   *
   * @return string[]
   *   An array of view displays, in the format VIEW_ID.DISPLAY_ID.
   */
  protected function getMediaLibraryViewDisplays(): array {
    $view_storage = $this->getViewStorage();

    $views =
      $view_storage->loadByProperties(['base_table' => 'media_field_data']);

    $displays = [];

    /** @var \Drupal\views\Entity\View $view */
    foreach ($views as $view) {
      foreach ($view->get('display') as $id => $display) {
        $view->getExecutable()->setDisplay($id);
        $display = $view->getExecutable()->getDisplay();

        if (!$display->usesFields() || !$display->hasPath()) {
          continue;
        }

        foreach ($display->options['fields'] as $field) {
          if ($field['id'] == 'media_library_select_form') {
            $display_id = sprintf("%s.%s", $view->id(), $id);

            $displays[$display_id] =
              sprintf(
                '%s - %s',
                $view->label(),
                $display->display['display_title']
              );
            break;
          }
        }
      }
    }

    return $displays;
  }

  /**
   * Detects whether the Media Library supports a configurable view display.
   *
   * This requires a patch from #2971209 to be applied to Core (or to be running
   * a version of Drupal that includes that feature).
   *
   * @return bool
   *   TRUE if Drupal core has been patched with support for configurable view
   *   displays.
   */
  protected static function doesMediaLibrarySupportCustomViewDisplay(): bool {
    // Detects changes introduced by
    // https://git.drupalcode.org/project/drupal/-/merge_requests/4019 as of
    // 3c872001742fa677e9e97eda8a96f911ec78c99f.
    $class_name = MediaLibraryState::class;

    $view_constant_name = 'DEFAULT_VIEW';
    $display_constant_name = 'DEFAULT_DISPLAY';

    return defined(implode('::', [$class_name, $view_constant_name])) &&
           defined(implode('::', [$class_name, $display_constant_name]));
  }

  /**
   * Determines whether the logged-in user can view the given media entity.
   *
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media entity on which to perform an access check.
   *
   * @return bool
   *   TRUE if the logged-in user can view the given entity.
   */
  protected function canView(MediaItemAdapterInterface $wrapped_media): bool {
    return $wrapped_media->canView();
  }

  /**
   * Determines whether the logged-in user can modify the given media entity.
   *
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media entity on which to perform an access check.
   *
   * @return bool
   *   TRUE if the logged-in user can modify the given entity.
   */
  protected function canModify(MediaItemAdapterInterface $wrapped_media): bool {
    return ($wrapped_media->canModify() &&
            !$this->shouldForceCloseForTranslation($wrapped_media));
  }

  /**
   * Determines whether the logged-in user can delete the given media entity.
   *
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media entity on which to perform an access check.
   *
   * @return bool
   *   TRUE if the logged-in user can delete the given entity.
   */
  protected function canDelete(MediaItemAdapterInterface $wrapped_media): bool {
    return $wrapped_media->canDelete();
  }

  /**
   * Gets the form display configured for editing the given media entity.
   *
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media adapter wrapping the media entity.
   *
   * @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface
   *   The form display.
   */
  protected function getFormDisplayMode(
        MediaItemAdapterInterface $wrapped_media): EntityFormDisplayInterface {
    $media_entity = $wrapped_media->toMediaEntity();

    // @codingStandardsIgnoreLine
    /** @noinspection PhpUnnecessaryLocalVariableInspection */
    $form_display =
      EntityFormDisplay::collectRenderDisplay(
        $media_entity,
        $this->getSetting('form_display_mode')
      );

    return $form_display;
  }

  /**
   * Gets the state of the widget for all deltas.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The state of the edit form.
   * @param array|null $parents
   *   Optional parameter for explicitly specifying the parents of the form
   *   element. If omitted, the parents are read from $this->fieldParents.
   *
   * @return array
   *   The state of the field widget.
   */
  protected function getCurrentWidgetState(
                                        FormStateInterface $form_state,
                                        array $parents = NULL): array {
    $field_name = $this->fieldDefinition->getName();

    if ($parents === NULL) {
      $parents = $this->fieldParents;
      assert($parents !== NULL);
    }

    $widget_state = static::getWidgetState($parents, $field_name, $form_state);
    assert(!empty($widget_state));

    if (!isset($widget_state['media'])) {
      $auto_collapse           = $this->getSetting('auto_collapse');
      $auto_collapse_threshold = $this->getSetting('auto_collapse_threshold');

      $widget_state['auto_collapse']            = $auto_collapse;
      $widget_state['auto_collapse_threshold']  = $auto_collapse_threshold;
      $widget_state['show_bulk_rename_options'] = FALSE;
      $widget_state['media']                    = [];
    }

    return $widget_state;
  }

  /**
   * Initializes the state of the edit widget for a single delta.
   *
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   All of the deltas/distinct values of the field.
   * @param int $delta
   *   The index of the delta being initialized.
   * @param array $widget_state
   *   A reference to the widget state for the delta being initialized.
   */
  protected function initializeWidgetStateForDelta(
                                                FieldItemListInterface $items,
                                                int $delta,
                                                array &$widget_state): void {
    $widget_delta_state =& $widget_state['media'][$delta];

    $wrapped_media = $widget_delta_state['wrapper'] ?? NULL;
    $item_mode     = $widget_delta_state['mode'] ?? NULL;

    assert(
      ($wrapped_media === NULL) ||
      ($wrapped_media instanceof MediaItemAdapterInterface)
    );

    $initial_mode = $this->getSetting('initial_mode');

    if (($wrapped_media === NULL) && isset($items[$delta]->entity)) {
      // This widget is not yet initialized, but we have an entity to use.
      $media_entity  = $items[$delta]->entity;
      $item_mode     = $initial_mode;
      $wrapped_media = MediaItemAdapter::create($media_entity);
    }

    assert(!empty($wrapped_media));

    $widget_delta_state['wrapper'] = $wrapped_media;
    $widget_delta_state['entity']  = $wrapped_media->toMediaEntity();
    $widget_delta_state['mode']    = $item_mode;
  }

  /**
   * Counts the number of media files in the editing mode specified.
   *
   * @param array $widget_state
   *   The state of the media file editing widget.
   * @param string $target_edit_mode
   *   The mode for which a count is desired.
   *
   * @return int
   *   The number of media files in the given mode.
   */
  protected function countMediaFilesInMode(array $widget_state,
                                           string $target_edit_mode): int {
    $media_files = $widget_state['media'];

    return array_reduce(
      $media_files,
      function (int $current_count, array $widget_delta_state) use ($target_edit_mode) {
        if ($widget_delta_state['mode'] === $target_edit_mode) {
          return $current_count + 1;
        }
        else {
          return $current_count;
        }
      },
      0
    );
  }

  /**
   * Counts the number of editable media files in the editing mode specified.
   *
   * @param array $widget_state
   *   The state of the media file editing widget.
   * @param string $target_edit_mode
   *   The mode for which a count is desired.
   *
   * @return int
   *   The number of editable media files in the given mode.
   */
  protected function countEditableMediaFilesInMode(array $widget_state,
                                                   string $target_edit_mode): int {
    $media_files = $widget_state['media'];

    return array_reduce(
      $media_files,
      function (int $current_count, array $widget_delta_state) use ($target_edit_mode) {
        $wrapper = $widget_delta_state['wrapper'];
        assert($wrapper instanceof MediaItemAdapterInterface);

        if ($this->canModify($wrapper) &&
            ($widget_delta_state['mode'] === $target_edit_mode)) {
          return $current_count + 1;
        }
        else {
          return $current_count;
        }
      },
      0
    );
  }

  /**
   * Builds a widget for arranging the order tabs appear in the media library.
   *
   * Adapted from
   * \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::settingsForm().
   *
   * @param array[] $media_types
   *   An associative array in which each key is the machine name of a media
   *   type, and the value is an array with the following keys:
   *     - label: The human-friendly name for the media type.
   *     - weight: The sort-order/display order of the media type.
   *
   * @return array
   *   A render array for the media tab widget.
   */
  protected function buildMediaTabSortWidget(array $media_types): array {
    $library_tab_order = [
      '#type'           => 'table',
      '#value_callback' => [static::class, 'storeMediaTabWeights'],
      '#header'         => [
        $this->t('Media type'),
        $this->t('Weight'),
      ],
      '#tabledrag'      => [
        [
          'action'       => 'order',
          'relationship' => 'sibling',
          'group'        => 'weight',
        ],
      ],
      '#attached'       => [
        'library' => [
          'inline_media_form/widget',
        ],
      ],
    ];

    foreach ($media_types as $media_type_id => $type_info) {
      $label  = $type_info['label'];
      $weight = $type_info['weight'];

      $library_tab_order[$media_type_id] = [
        '#weight'     => $weight,
        '#attributes' => ['class' => ['draggable']],
        'label'       => ['#markup' => $label],
        'weight'      => [
          '#type'          => 'weight',
          '#title'         => t('Weight for @title', ['@title' => $label]),
          '#title_display' => 'invisible',
          '#default_value' => $weight,
          '#attributes'    => [
            'class' => ['weight'],
          ],
        ],
      ];
    }

    return $library_tab_order;
  }

  /**
   * Builds the widget for collapsing/editing multiple deltas/media files.
   *
   * @param array $form
   *   The entity edit form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The state of the edit form.
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   All of the deltas/distinct values of the field.
   * @param array $elements
   *   The base render array for the top-level widget form element.
   *
   * @return array
   *   The fully-populated render array for the widget.
   */
  protected function buildWidgetForEditMode(array &$form,
                                            FormStateInterface $form_state,
                                            FieldItemListInterface $items,
                                            array &$elements): array {
    $max = $this->realItemCount;

    $field_definition         = $this->fieldDefinition;
    $field_storage_definition = $field_definition->getFieldStorageDefinition();

    $field_name  = $field_definition->getName();
    $field_title = $field_definition->getLabel();
    $is_required = $field_definition->isRequired();

    $is_multiple = $field_storage_definition->isMultiple();
    $cardinality = $field_storage_definition->getCardinality();

    $token_service = \Drupal::token();

    $description =
      FieldFilteredMarkup::create(
        $token_service->replace($field_definition->getDescription())
      );

    if ($max > 0) {
      for ($delta = 0; $delta < $max; ++$delta) {
        // Add a new empty item if it doesn't exist yet at this delta.
        if (!isset($items[$delta])) {
          $items->appendItem();
        }

        // For multiple fields, title and description are handled by the
        // wrapping table.
        $current_element = [
          '#title'       => $is_multiple ? '' : $field_title,
          '#description' => $is_multiple ? '' : $description,
        ];

        $current_element =
          $this->formSingleElement(
            $items,
            $delta,
            $current_element,
            $form,
            $form_state
          );

        if (!empty($current_element)) {
          // Input field for the delta (drag-n-drop reordering).
          if ($is_multiple) {
            // We name the element '_weight' to avoid clashing with elements
            // defined by widget.
            $current_element['_weight'] = [
              '#type'          => 'weight',
              '#title_display' => 'invisible',
              '#title'         => $this->t(
                'Weight for row @number',
                ['@number' => $delta + 1]
              ),

              // Note: this 'delta' is the FAPI #type 'weight' element's
              // property.
              '#delta'         => $max,
              '#default_value' => $items[$delta]->_weight ?: $delta,
              '#weight'        => 100,
            ];
          }

          $has_access = $current_element['#access'] ?? TRUE;

          // Access for the top element is set to FALSE only when the media file
          // is removed. Media file access checking is done at a lower level.
          if (!$has_access) {
            --$this->realItemCount;
          }
          else {
            $elements[$delta] = $current_element;
          }
        }
      }
    }

    // NOTE: Widget state for each delta gets populated during
    // formSingleElement() calls above.
    $widget_state = $this->getCurrentWidgetState($form_state);
    $widget_state['real_item_count'] = $this->realItemCount;

    static::setWidgetState(
      $this->fieldParents,
      $field_name,
      $form_state,
      $widget_state
    );

    $referenceable_bundles = $this->getAllowedTypes($field_definition);

    $target_bundles =
      array_map(
        function ($bundle_info) {
          return $bundle_info['label'];
        },
        $referenceable_bundles
      );

    $title_label  = $this->getSetting('title');
    $title_plural = $this->getSetting('title_plural');

    $elements += [
      '#element_validate'       => [[$this, 'multipleElementValidate']],
      '#required'               => $is_required,
      '#field_name'             => $field_name,
      '#cardinality'            => $cardinality,
      '#max_delta'              => $max - 1,
      '#target_bundles'         => $target_bundles,
      '#reference_title'        => $title_label,
      '#reference_title_plural' => $title_plural,
    ];

    $elements += [
      '#theme'                => 'field_multiple_value_form',
      '#field_name'           => $field_name,
      '#cardinality'          => $cardinality,
      '#cardinality_multiple' => TRUE,
      '#required'             => $is_required,
      '#title'                => $field_title,
      '#description'          => $description,
      '#max_delta'            => $max - 1,
    ];

    /** @var \Drupal\Core\Entity\ContentEntityInterface $host_entity */
    $host_entity = $items->getEntity();
    assert($host_entity instanceof ContentEntityInterface);

    $this->detectTranslationMode($host_entity, $form_state);

    $header_actions =
      $this->buildHeaderActions(
        $form,
        $form_state,
        $items,
        $elements,
        $widget_state
      );

    if (!empty($header_actions)) {
      // Add a weight element so we guarantee that header actions will stay in
      // first row. We will use this later in
      // inline_media_form_preprocess_field_multiple_value_form().
      $header_actions['_weight'] = [
        '#type'          => 'weight',
        '#default_value' => -100,
      ];

      $elements['header_actions'] = $header_actions;
    }

    $is_cardinality_unlimited =
      ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);

    $has_room             = ($this->realItemCount < $cardinality);
    $all_items_accessible = empty($widget_state['inaccessible_item_count']);

    // Disable ability to adjust weights if the user cannot view all the deltas.
    $can_reorder_deltas =
      $all_items_accessible && $this->allowReferenceChanges();

    if (($this->realItemCount > 0) && !$all_items_accessible) {
      $warning_message =
        $this->createMessage(
          $this->t(
            'Items cannot be re-ordered because access to some @title is restricted. Only accessible @title are shown below.',
            ['@title' => $this->getSetting('title_plural')]
          )
        );

      $warning_message['#weight']  = -100;
      $elements['reorder_warning'] = $warning_message;
    }

    if (($is_cardinality_unlimited || $has_room) &&
        !$form_state->isProgrammed() && $this->allowReferenceChanges()) {
      $triggering_element = $form_state->getTriggeringElement();

      $elements['add_more'] =
        $this->buildOpenerRegion(
          $host_entity,
          $field_name,
          $cardinality,
          $widget_state,
          $triggering_element
        );

      $elements['add_more']['#attributes']['class'][] =
        'inline-media-form-add-wrapper';
    }

    $elements['#allow_reference_changes']  = $can_reorder_deltas;
    $elements['#inline_media_form_widget'] = TRUE;
    $elements['#attached']['library'][]    = 'inline_media_form/widget';

    $any_files_open =
      ($this->countMediaFilesInMode($widget_state, self::EDIT_MODE_OPEN) > 0);

    $elements['#attributes']['class'][] = 'media-widget';

    if ($any_files_open) {
      // This allow us to hide the draggable "grippies" for weight when any of
      // the media files are open for editing.
      $elements['#attributes']['class'][] = 'media-widget--has-open-files';
    }
    else {
      $elements['#attributes']['class'][] = 'media-widget--all-files-closed';
    }

    return $elements;
  }

  /**
   * Builds header actions.
   *
   * @param array $form
   *   The entity edit form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The state of the edit form.
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   All of the deltas/distinct values of the field.
   * @param array $element
   *   The form element render array containing the basic properties for the
   *   top-level widget.
   * @param array $widget_state
   *   Field widget state.
   *
   * @return array[]
   *   The form element array.
   */
  protected function buildHeaderActions(array $form,
                                        FormStateInterface $form_state,
                                        FieldItemListInterface $items,
                                        array $element,
                                        array $widget_state): array {
    $actions    = [];
    $field_name = $this->fieldDefinition->getName();
    $id_prefix  = implode('-', array_merge($this->fieldParents, [$field_name]));

    $open_count =
      $this->countMediaFilesInMode($widget_state, self::EDIT_MODE_OPEN);

    $closed_editable_count =
      $this->countEditableMediaFilesInMode(
        $widget_state,
        self::EDIT_MODE_CLOSED
      );

    // Bulk rename option.
    if (($widget_state['show_bulk_rename_options'] === FALSE) &&
        ($open_count === 0) && ($closed_editable_count != 0)) {
      // Do not show option for bulk rename if:
      // 1. There are no media files; OR
      // 2. At least one media file is open for editing; OR
      // 3. The bulk rename toolbar is already open.
      $actions['actions']['bulk_rename'] =
        $this->buildBulkRenameButton($field_name, $id_prefix);
    }

    $toolbar = $this->buildToolbar($widget_state);

    $actions['toolbar'] = $toolbar;

    // Collapse & expand all.
    if (($this->fieldDefinition->getType() == 'entity_reference') &&
        ($this->realItemCount > 1) &&
        $this->isFeatureEnabled(self::FEATURE_COLLAPSE_EDIT_ALL)) {
      if ($open_count != 0) {
        $actions['actions']['collapse_all'] =
          $this->buildCollapseAllButton($field_name, $id_prefix);
      }

      if ($closed_editable_count != 0) {
        $actions['actions']['edit_all'] = $this->buildEditAllButton($id_prefix);
      }
    }

    $this->alterWidgetHeaderActions(
      $form,
      $form_state,
      $items,
      $element,
      $actions
    );

    // Add inline_media_form_header flag which we use later in preprocessor to
    // move header actions to table header.
    if ($actions) {
      // Set actions.
      $actions['#type']                     = 'inline_media_form_actions';
      $actions['#inline_media_form_header'] = TRUE;
    }

    return $actions;
  }

  /**
   * Builds the render array for the toolbar sub-component of the actions bar.
   *
   * This region can be empty if there is no toolbar to display. It is used for
   * requesting additional parameters from users during short interactions like
   * bulk renaming of media files.
   *
   * @param array $widget_state
   *   Field widget state.
   *
   * @return array
   *   The render array for the elements of the toolbar.
   */
  protected function buildToolbar(array $widget_state): array {
    $toolbar = [];

    if ($widget_state['show_bulk_rename_options'] === TRUE) {
      $toolbar = [
        'bulk_rename' => $this->buildBulkRenameToolbar(),
      ];
    }

    return $toolbar;
  }

  /**
   * Builds the toolbar for getting settings for bulk rename.
   *
   * @return array
   *   The render array for the bulk rename toolbar.
   */
  protected function buildBulkRenameToolbar(): array {
    $toolbar = [
      '#type' => 'container',
      '#title' => $this->t('Bulk rename'),
      '#attributes' => [
        'class' => [
          'inline-media-form-toolbar',
          'inline-media-form-toolbar--bulk-rename',
        ],
      ],
    ];

    $toolbar['name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Bulk rename to'),
      '#required' => TRUE,
    ];

    $toolbar['number_sequentially'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Sequentially number media in the order they appear below.'),
      '#default_value' => TRUE,
    ];

    $limit_validation_errors = [
      [
        $this->getFieldName(),
        'header_actions',
        'toolbar',
        'bulk_rename',
        'name',
      ],
    ];

    $toolbar['rename'] = [
      '#type'                    => 'submit',
      '#value'                   => $this->t('Rename'),
      '#button_type'             => 'primary',
      '#limit_validation_errors' => $limit_validation_errors,
      '#submit'                  => [
        [static::class, 'bulkRenameAllMediaSubmit'],
      ],
      '#ajax'                    => [
        'effect'   => 'fade',
        'progress' => ['type' => 'fullscreen'],
        'callback' => [static::class, 'handleToolbarAjax'],
        'wrapper'  => $this->fieldWrapperId,
      ],
    ];

    $toolbar['cancel'] = [
      '#type'                    => 'submit',
      '#value'                   => $this->t('Cancel'),
      '#limit_validation_errors' => $limit_validation_errors,
      '#submit'                  => [
        [static::class, 'hideBulkRenameToolbarSubmit'],
      ],
      '#ajax'                    => [
        'effect'   => 'fade',
        'progress' => ['type' => 'fullscreen'],
        'callback' => [static::class, 'handleToolbarAjax'],
        'wrapper'  => $this->fieldWrapperId,
      ],
    ];

    return $toolbar;
  }

  /**
   * Builds the edit/summary widget for a single media file.
   *
   * @param array $form
   *   The entity edit form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   * @param string $field_name
   *   The machine name of the field.
   * @param array $widget_state
   *   The state of all of the widgets of this field.
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   All of the deltas/distinct values of the field.
   * @param array $element
   *   The base widget form element render array.
   * @param int $delta
   *   The index of the value that the widget is being constructed for.
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media file for which a widget is being constructed.
   *
   * @return array
   *   The fully-populated render array for the widget.
   */
  protected function buildSingleWidget(
                              array $form,
                              FormStateInterface $form_state,
                              string $field_name,
                              array &$widget_state,
                              FieldItemListInterface $items,
                              array $element,
                              int $delta,
                              MediaItemAdapterInterface $wrapped_media): array {
    $parents = $element['#field_parents'];

    $widget_delta_state =& $widget_state['media'][$delta];

    $media_type  = $this->getFieldSetting('target_type');
    $title_label = $this->getSetting('title');

    $media_bundle = $wrapped_media->getMediaType();
    $media_entity = $wrapped_media->toMediaEntity();

    $item_mode           = $widget_delta_state['mode'] ?? NULL;
    $has_unsaved_changes = $wrapped_media->isChanged();

    if ($this->shouldForceCloseForTranslation($wrapped_media)) {
      $item_mode = self::EDIT_MODE_CLOSED;
    }

    $id_prefix = implode('-', array_merge($parents, [$field_name, $delta]));

    $element =
      $this->prepareSingleWidget(
        $media_bundle,
        $parents,
        $field_name,
        $delta,
        $id_prefix,
        $element,
        $has_unsaved_changes
      );

    $status_icons = [];

    $can_update_entity    = $this->canModify($wrapped_media);
    $preview_display_mode = $this->getSetting('preview_display_mode');

    $view_display =
      $this->entityDisplayRepository->getViewDisplay(
        $media_type,
        $media_bundle,
        $preview_display_mode
      );

    $media_teaser = $view_display->build($media_entity);

    $element['top']['preview']['teaser'] = [
      '#prefix' => '<div class="inline-media-file-preview-teaser">',
      '#suffix' => '</div>',
      '#weight' => 1,
      'content' => $media_teaser,
    ];

    // Widget actions.
    $widget_actions = [
      'actions'          => [],
      'dropdown_actions' => [],
    ];

    if ($item_mode != self::EDIT_MODE_REMOVED) {
      $widget_actions['dropdown_actions']['remove_button'] =
        $this->buildRemoveButton($widget_state, $id_prefix, $delta);

      // Force the closed mode when the user cannot edit the media file.
      if (!$can_update_entity) {
        $item_mode = self::EDIT_MODE_CLOSED;
      }
    }

    if ($item_mode == self::EDIT_MODE_OPEN) {
      $widget_actions['actions']['collapse_button'] =
        $this->buildCollapseButton(
          $field_name,
          $widget_state,
          $delta,
          $id_prefix,
          $parents,
          $wrapped_media
        );
    }
    else {
      $widget_actions['actions']['edit_button'] =
        $this->buildEditButton(
          $field_name,
          $widget_state,
          $delta,
          $id_prefix,
          $parents,
          $wrapped_media
        );

      if ($wrapped_media->isChanged()) {
        $status_icons['changed'] = [
          '#theme'   => 'inline_media_form_info_icon',
          '#icon'    => 'changed',
          '#message' => $this->t(
            'You have unsaved changes on this @title item.',
            ['@title' => $title_label]
          ),
        ];
      }

      if (!$wrapped_media->isPublished()) {
        $status_icons['preview'] = [
          '#theme'   => 'inline_media_form_info_icon',
          '#icon'    => 'unpublished',
          '#message' => $this->t('Unpublished'),
        ];
      }
    }

    // If update is disabled we will show the disabled pencil icon.
    if (!$can_update_entity) {
      $widget_actions['actions']['edit_disabled'] = [
        '#theme'   => 'inline_media_form_info_icon',
        '#icon'    => 'edit-disabled',
        '#weight'  => 1,
        '#message' => $this->t(
          'You are not allowed to edit this @title.',
          ['@title' => $title_label]
        ),
      ];
    }

    $this->alterWidgetDeltaActions(
      $form,
      $form_state,
      $items,
      $delta,
      $element,
      $wrapped_media,
      $widget_actions
    );

    if (!empty($widget_actions['actions'])) {
      // Expand all actions to proper submit elements and add it to top
      // actions sub component.
      $element['top']['actions']['actions'] =
        array_map(
          [$this, 'buildActionButton'],
          $widget_actions['actions']
        );
    }

    if (!empty($widget_actions['dropdown_actions'])) {
      // Expand all dropdown actions to proper submit elements and add
      // them to top dropdown actions sub component.
      $element['top']['actions']['dropdown_actions'] =
        array_map(
          [$this, 'buildActionButton'],
          $widget_actions['dropdown_actions']
        );
    }

    $form_display = $this->getFormDisplayMode($wrapped_media);

    // @todo Remove as part of https://www.drupal.org/node/2640056
    if ($this->moduleHandler->moduleExists('field_group')) {
      $element =
        $this->applyFieldGroupWorkaroundForIssue2640056(
          $element,
          $media_entity,
          $form_display
        );
    }

    if ($item_mode == self::EDIT_MODE_OPEN) {
      $element =
        $this->buildOpenWidget(
          $element,
          $form_state,
          $form_display,
          $wrapped_media
        );
    }
    elseif ($item_mode == self::EDIT_MODE_CLOSED) {
      $element =
        $this->buildClosedWidget(
          $element,
          $form_display,
          $wrapped_media
        );
    }
    else {
      $element['subform'] = [];
    }

    // If we have any info items lets add them to the top section.
    if (!empty($status_icons)) {
      foreach ($status_icons as $info_item) {
        if (!isset($info_item['#access']) || $info_item['#access']) {
          $element['top']['icons']['items'] = $status_icons;
          break;
        }
      }
    }

    $element['subform']['#attributes']['class'][] = 'inline-media-form-subform';
    $element['subform']['#access'] = $can_update_entity;

    if ($item_mode == self::EDIT_MODE_REMOVED) {
      $element['#access'] = FALSE;
    }

    $widget_delta_state['wrapper'] = $wrapped_media;
    $widget_delta_state['entity']  = $wrapped_media->toMediaEntity();
    $widget_delta_state['display'] = $form_display;
    $widget_delta_state['mode']    = $item_mode;

    return $element;
  }

  /**
   * Adds standard boilerplate container IDs, CSS classes, and callbacks.
   *
   * @param string $media_type_id
   *   The machine name of the bundle/media type for which the widget is being
   *   prepared.
   * @param string[] $parents
   *   The names of each form element under which the widget is nested.
   * @param string $field_name
   *   The machine name of the field.
   * @param int $delta
   *   The index of the value that the widget is being constructed for.
   * @param string $id_prefix
   *   A unique prefix to use for the elements of this field and delta.
   * @param array $element
   *   The base widget form element render array.
   * @param bool $has_unsaved_changes
   *   Whether the widget has changes that have not yet been persisted to the
   *   DB.
   *
   * @return array
   *   The form element, with boilerplate properties added.
   */
  protected function prepareSingleWidget(string $media_type_id,
                                         array $parents,
                                         string $field_name,
                                         int $delta,
                                         string $id_prefix,
                                         array $element,
                                         bool $has_unsaved_changes): array {
    $wrapper_id = Html::getUniqueId($id_prefix . '-item-wrapper');

    $element_parents   = $parents;
    $element_parents[] = $field_name;
    $element_parents[] = $delta;
    $element_parents[] = 'subform';

    $element += [
      '#type'             => 'container',
      '#element_validate' => [[$this, 'elementValidate']],
      '#media_file_type'  => $media_type_id,
      'subform'           => [
        '#type'    => 'container',
        '#parents' => $element_parents,
      ],
    ];

    $element['#prefix'] = '<div id="' . $wrapper_id . '">';
    $element['#suffix'] = '</div>';

    // Create top section structure with all needed subsections.
    $element['top'] = [
      '#type'       => 'container',
      '#weight'     => -1000,
      '#attributes' => [
        'class' => [
          'inline-media-file-top',
          'add-above-off',
        ],
      ],

      // Section for a preview of the media file.
      'preview' => [
        '#type'       => 'container',
        '#attributes' => ['class' => ['inline-media-file-preview']],
      ],

      // Section for info icons.
      'icons' => [
        '#type'       => 'container',
        '#attributes' => ['class' => ['inline-media-file-info']],
      ],

      'summary' => [
        '#type'       => 'container',
        '#attributes' => ['class' => ['inline-media-file-summary']],
      ],

      // Element for actions and dropdown actions on media files.
      'actions' => [
        '#type' => 'inline_media_form_actions',
      ],
    ];

    if ($has_unsaved_changes) {
      // Make it possible for us to call attention to the summary if the file
      // has unsaved changes.
      $element['top']['summary']['#attributes']['class'][] =
        'inline-media-file-summary--changed';
    }

    return $element;
  }

  /**
   * Adjusts edit mode and entity being edited, as appropriate for translation.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $host_entity
   *   The parent entity being edited.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The state of the entity edit form.
   * @param array $widget_delta_state
   *   A reference to the state of the widget for the delta currently being
   *   rendered.
   */
  protected function applyTranslation(ContentEntityInterface $host_entity,
                                      FormStateInterface $form_state,
                                      array &$widget_delta_state): void {
    $langcode      = $form_state->get('langcode');
    $wrapped_media = $widget_delta_state['wrapper'] ?? NULL;

    assert(
      ($wrapped_media === NULL) ||
      ($wrapped_media instanceof MediaItemAdapterInterface)
    );

    $item_mode    = $widget_delta_state['mode'];
    $media_entity = $wrapped_media->toMediaEntity();

    // Detect if we are translating.
    $this->detectTranslationMode($host_entity, $form_state);

    if ($this->isTranslating) {
      assert($host_entity instanceof TranslationStatusInterface);
      $host_translation_status = $host_entity->getTranslationStatus($langcode);

      $host_translation_was_created =
        ($host_translation_status == TranslationStatusInterface::TRANSLATION_CREATED);

      // If the node is being translated, the media files should be all open
      // when the form is not being rebuilt (E.g. when clicking on a media
      // files action) and when the translation is being added.
      if (!$form_state->isRebuilding() && $host_translation_was_created) {
        $item_mode = self::EDIT_MODE_OPEN;
      }

      // Add translation if missing for the target language.
      if (!$media_entity->hasTranslation($langcode)) {
        // Get the selected translation of the media file entity.
        $entity_langcode = $media_entity->language()->getId();
        $source          = $form_state->get(['content_translation', 'source']);
        $source_langcode = $source ? $source->getId() : $entity_langcode;

        // Make sure the source language version is used if available. It is a
        // the host and fetching the translation without this check could lead
        // valid scenario to have no media files items in the source version of
        // to an exception.
        if ($media_entity->hasTranslation($source_langcode)) {
          $media_entity = $media_entity->getTranslation($source_langcode);
        }

        // The media files entity has no content translation source field if
        // no media file entity field is translatable, even if the host is.
        if ($media_entity->hasField('content_translation_source')) {
          // Initialise the translation with source language values.
          $media_entity->addTranslation($langcode, $media_entity->toArray());
          $translation = $media_entity->getTranslation($langcode);

          $this->translationManager
            ->getTranslationMetadata($translation)
            ->setSource($media_entity->language()->getId());
        }
      }

      // If any media files type is translatable do not switch.
      if ($media_entity->hasField('content_translation_source')) {
        // Switch the media file to the translation.
        $media_entity = $media_entity->getTranslation($langcode);
      }
    }
    else {
      // Set the langcode, since we are not translating.
      $langcode_key = $media_entity->getEntityType()->getKey('langcode');

      if ($media_entity->get($langcode_key)->value != $langcode) {
        // If a translation in the given language already exists, switch to
        // that. If there is none yet, update the language.
        if ($media_entity->hasTranslation($langcode)) {
          $media_entity = $media_entity->getTranslation($langcode);
        }
        else {
          $media_entity->set($langcode_key, $langcode);
        }
      }
    }

    $wrapped_media = MediaItemAdapter::create($media_entity);

    $widget_delta_state['wrapper'] = $wrapped_media;
    $widget_delta_state['entity']  = $wrapped_media->toMediaEntity();
    $widget_delta_state['mode']    = $item_mode;
  }

  /**
   * Builds the widget region for opening the media browser.
   *
   * @param \Drupal\Core\Entity\EntityInterface $host_entity
   *   The entity containing the media field.
   * @param string $field_name
   *   The machine name of the field.
   * @param int $cardinality
   *   The total number of media files that can be selected by the user. Can be
   *   FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED.
   * @param array $widget_state
   *   The current widget state.
   * @param array|null $triggering_element
   *   If the form is rebuilding, the element that triggered the rebuild of the
   *   form; or, NULL, if the form is not rebuilding.
   *
   * @return array
   *   The form element array.
   */
  protected function buildOpenerRegion(EntityInterface $host_entity,
                                       string $field_name,
                                       int $cardinality,
                                       array $widget_state,
                                       ?array $triggering_element): array {
    $elements     = [];
    $plural_label = $this->getSetting('title_plural');

    if ($this->user->hasPermission('view media')) {
      // All the media types the field can reference (regardless of user).
      $referenceable_media_type_ids = array_keys($this->getAllowedTypes());

      if (count($referenceable_media_type_ids) === 0) {
        $elements['icons'] =
          $this->createMessage(
            $this->t(
              'This field is not yet configured to allow @title to be referenced.',
              ['@title' => $plural_label]
            )
          );
      }
      else {
        $referenced_media_ids = $this->getReferencedMediaIds($widget_state);

        $elements =
          $this->buildMediaLibraryOpener(
            $host_entity,
            $field_name,
            $cardinality,
            $referenceable_media_type_ids,
            $referenced_media_ids,
            $triggering_element
          );
      }
    }
    else {
      $elements['icons'] =
        $this->createMessage(
          $this->t(
            'You are not allowed to view or add any @title.',
            ['@title' => $plural_label]
          )
        );
    }

    return $elements;
  }

  /**
   * Builds the button for selecting a media file to be added.
   *
   * @param \Drupal\Core\Entity\EntityInterface $host_entity
   *   The entity containing the media field.
   * @param string $field_name
   *   The machine name of the field.
   * @param int $cardinality
   *   The total number of media files that can be selected by the user. Can be
   *   FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED.
   * @param string[] $allowed_media_type_ids
   *   The machine names of the media types that can be selected.
   * @param int[] $referenced_entity_ids
   *   The unique identifiers of the media files that have been selected so far.
   * @param array|null $triggering_element
   *   If the form is rebuilding, the element that triggered the rebuild of the
   *   form; or, NULL, if the form is not rebuilding.
   *
   * @return array
   *   The form element array.
   */
  protected function buildMediaLibraryOpener(
                                           EntityInterface $host_entity,
                                           string $field_name,
                                           int $cardinality,
                                           array $allowed_media_type_ids,
                                           array $referenced_entity_ids,
                                           ?array $triggering_element): array {
    $has_limited_cardinality =
      ($cardinality !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);

    $selected_count  = count($referenced_entity_ids);
    $field_widget_id = $this->fieldIdPrefix;

    $element = [
      '#type'           => 'container',
      '#cardinality'    => $cardinality,
      '#attributes'     => [
        'id'    => $field_widget_id,
        'class' => ['js-media-library-widget'],
      ],
      '#attached' => [
        'library' => ['media_library/widget'],
      ],
    ];

    if ($has_limited_cardinality) {
      $remaining_count = $cardinality - $selected_count;

      $title_label  = $this->getSetting('title');
      $title_plural = $this->getSetting('title_plural');

      if ($remaining_count !== 0) {
        $cardinality_message =
          $this->formatPlural(
            $remaining_count,
            'One more @title can be selected.',
            '@count more @title_plural can be selected.',
            [
              '@title'        => $title_label,
              '@title_plural' => $title_plural,
            ]
          );
      }
      else {
        $cardinality_message =
          $this->t(
            'The maximum number of @title_plural have been selected.',
            ['@title_plural' => $title_plural]
          );
      }

      // Add a line break between the field message and the cardinality message.
      if (!empty($element['#description'])) {
        $element['#description'] .= '<br />';
      }

      $element['#description'] .= $cardinality_message;
    }
    else {
      $remaining_count = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
    }

    $state = [
      'host_entity'            => $host_entity,
      'field_widget_id'        => $field_widget_id,
      'field_name'             => $field_name,
      'referenced_entity_ids'  => $referenced_entity_ids,
      'allowed_media_type_ids' => $allowed_media_type_ids,
      'remaining_count'        => $remaining_count,
      'library_view_display'   => $this->getSetting('library_view_display'),
    ];

    $button_text =
      $this->t(
        'Add @title',
        ['@title' => $this->getSetting('title_plural')]
      );

    $unique_opener_name = $field_widget_id . '-media-library-open-button';
    $unique_update_name = $field_widget_id . '-media-library-update';

    // Add a button that will load the Media library in a modal using AJAX.
    $open_button = [
      '#type'                       => 'button',
      '#value'                      => $button_text,
      '#name'                       => $unique_opener_name,
      '#media_library_opener_state' => $state,

      // Allow the media library to be opened even if there are form errors.
      '#limit_validation_errors' => [],

      '#attributes'              => [
        'class' => [
          'js-media-library-open-button',
        ],
        // The jQuery UI dialog automatically moves focus to the first :tabbable
        // element of the modal, so we need to disable refocus on the button.
        'data-disable-refocus' => 'true',
      ],
      '#ajax'                    => [
        'callback' => [static::class, 'handleOpenMediaLibraryAjax'],
        'progress' => [
          'type'    => 'throbber',
          'message' => $this->t('Opening media library.'),
        ],
      ],
    ];

    // When the user returns from the modal to the widget, we want to shift the
    // focus back to the open button. If the user is not allowed to add more
    // items, the button needs to be disabled. Since we can't shift the focus to
    // disabled elements, the focus is set back to the open button via
    // JavaScript by adding the 'data-disabled-focus' attribute.
    // @see Drupal.behaviors.MediaLibraryWidgetDisableButton
    if ($has_limited_cardinality && ($remaining_count === 0)) {
      if (!empty($triggering_element) &&
          ($trigger_parents = $triggering_element['#array_parents']) &&
           end($trigger_parents) === 'media_library_update_widget') {
        // The widget is being rebuilt from a selection change.
        $open_button['#attributes']['data-disabled-focus'] = 'true';
      }
      else {
        // The widget is being built without a selection change, so we can just
        // set the item to disabled now, there is no need to set the focus
        // first.
        $open_button['#disabled'] = TRUE;
      }

      $open_button['#attributes']['class'][] = 'visually-hidden';
    }

    $element['open_button'] = $open_button;

    // This hidden field and button are used to add new items to the widget.
    $element['media_library_selection'] = [
      '#type'       => 'hidden',
      '#attributes' => [
        // This is used to pass the selection from the modal to the widget.
        'data-media-library-widget-value' => $field_widget_id,
      ],
    ];

    // We need to prevent the widget from being validated when no media items
    // are selected. When a media field is added in a subform, entity
    // validation is triggered in EntityFormDisplay::validateFormValues().
    // Since the media item is not added to the form yet, this triggers errors
    // for required media fields.
    if (empty($referenced_entity_ids)) {
      $limit_validation_errors = [];
    }
    else {
      $limit_validation_errors =
        [array_merge($this->fieldParents, [$field_name])];
    }

    // When a selection is made this hidden button is pressed to add new media
    // items based on the "media_library_selection" value.
    $element['media_library_update_widget'] = [
      '#type'                    => 'submit',
      '#value'                   => $this->t('Update widget'),
      '#name'                    => $unique_update_name,
      '#limit_validation_errors' => $limit_validation_errors,
      '#validate'                => [
        [static::class, 'validateSelectedMediaItems'],
      ],
      '#submit'                  => [
        [static::class, 'addSelectedMediaItemSubmit'],
      ],
      '#attributes'              => [
        'data-media-library-widget-update' => $field_widget_id,
        'class'                            => ['js-hide'],
      ],
      '#ajax'                    => [
        'callback' => [static::class, 'handleMediaSelectedAjax'],
        'wrapper'  => $this->fieldWrapperId,
        'progress' => [
          'type'    => 'throbber',
          'message' => $this->t('Adding selection.'),
        ],
      ],
    ];

    return $element;
  }

  /**
   * Helper to create a media file UI message.
   *
   * @param string $message
   *   Message text.
   * @param string $type
   *   Message type.
   *
   * @return array
   *   Render array of message.
   */
  protected function createMessage(string $message,
                                   string $type = 'warning'): array {
    return [
      '#type'       => 'container',
      '#markup'     => $message,
      '#attributes' => ['class' => ['messages', 'messages--' . $type]],
    ];
  }

  /**
   * Prepares the widget state to add a new media file at a specific position.
   *
   * In addition to the widget state change, also user input could be modified
   * to handle adding of a new media file at a specific position between
   * existing media files.
   *
   * @param array $widget_state
   *   A reference to the state of the widget.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   * @param array $field_path
   *   Path to inline media form field.
   * @param int $target_delta
   *   Delta position in list of media files, where new media file will be
   *   added.
   */
  protected static function prepareDeltaPosition(array &$widget_state,
                                                 FormStateInterface $form_state,
                                                 array $field_path,
                                                 int $target_delta): void {
    // Increase number of items to create place for new media file.
    ++$widget_state['items_count'];
    ++$widget_state['real_item_count'];

    // Change user input in order to create new delta position.
    $user_input  =& $form_state->getUserInput();
    $field_input = NestedArray::getValue($user_input, $field_path);

    // Rearrange all original deltas to make one place for the new element.
    $new_deltas      = [];
    $original_deltas = $widget_state['original_deltas'] ?? [];

    foreach ($original_deltas as $current_delta => $original_delta) {
      if (($current_delta >= $target_delta)) {
        $new_delta = ($current_delta + 1);
      }
      else {
        $new_delta = $current_delta;
      }

      $new_deltas[$new_delta]                  = $original_delta;
      $field_input[$original_delta]['_weight'] = $new_delta;
    }

    // Add information into delta mapping for the new element.
    $original_deltas_size                          = count($original_deltas);
    $new_deltas[$target_delta]                     = $original_deltas_size;
    $field_input[$original_deltas_size]['_weight'] = $target_delta;

    $widget_state['original_deltas'] = $new_deltas;

    NestedArray::setValue($user_input, $field_path, $field_input);
  }

  /**
   * Re-sorts media files based on their new weights.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param array $field_values_parents
   *   The field value parents.
   *
   * @noinspection PhpDocMissingThrowsInspection
   * @noinspection PhpUnhandledExceptionInspection
   *   If deltas are missing, we have a code bug that could corrupt data; so a
   *   fatal exception is acceptable under those circumstances.
   */
  protected static function reorderMediaFiles(
                                            FormStateInterface $form_state,
                                            array $field_values_parents): void {
    $field_name = end($field_values_parents);

    $field_values =
      NestedArray::getValue($form_state->getValues(), $field_values_parents);

    $complete_field_storage = NestedArray::getValue(
      $form_state->getStorage(), [
        'field_storage',
        '#parents',
      ]
    );

    $new_field_storage = $complete_field_storage;

    // Set a flag to prevent this from running twice, as the entity is built
    // for validation as well as saving and would fail the second time as we
    // already altered the field storage.
    if (!empty($new_field_storage['#fields'][$field_name]['reordered'])) {
      return;
    }

    $new_field_storage['#fields'][$field_name]['reordered'] = TRUE;

    // Clear out all current media file keys in all nested media file widgets
    // as there might be fewer than before or none in a certain widget.
    $clear_media_files = function ($field_storage) use (&$clear_media_files) {
      foreach ($field_storage as $key => $value) {
        if ($key === '#fields') {
          foreach ($value as $field_name => $widget_state) {
            if (isset($widget_state['media'])) {
              $field_storage['#fields'][$field_name]['media'] = [];
            }
          }
        }
        else {
          $field_storage[$key] = $clear_media_files($field_storage[$key]);
        }
      }

      return $field_storage;
    };

    // Only clear the current field and its children to avoid deleting
    // media file references in other fields.
    $new_field_storage['#fields'][$field_name]['media'] = [];

    if (isset($new_field_storage[$field_name])) {
      $new_field_storage[$field_name] =
        $clear_media_files($new_field_storage[$field_name]);
    }

    $reorder_media = function ($reorder_values, $parents = [], FieldableEntityInterface $parent_entity = NULL) use ($complete_field_storage, &$new_field_storage, &$reorder_media) {
      foreach ($reorder_values as $field_name => $values) {
        foreach ($values['list'] as $delta => $item_values) {
          $old_keys = array_merge(
            $parents, [
              '#fields',
              $field_name,
              'media',
              $delta,
            ]
          );

          $path           = explode('][', $item_values['_path']);
          $new_field_name = array_pop($path);
          $key_parents    = [];

          foreach ($path as $i => $key) {
            $key_parents[] = $key;

            if ($i % 2 == 1) {
              $key_parents[] = 'subform';
            }
          }

          $new_keys = array_merge(
            $key_parents, [
              '#fields',
              $new_field_name,
              'media',
              $item_values['_weight'],
            ]
          );

          $key_exists = NULL;

          $item_state =
            NestedArray::getValue(
              $complete_field_storage,
              $old_keys,
              $key_exists
            );

          if (!$key_exists && $parent_entity) {
            // If key does not exist, then this parent widget was previously
            // not expanded. This can only happen on nested levels. In that
            // case, initialize a new item state and set the widget state to
            // an empty array if it is not already set from an earlier item.
            // If something else is placed there, it will be put in there,
            // otherwise the widget will know that nothing is there anymore.
            $item_state = [
              'entity' => $parent_entity->get($field_name)->get($delta)->entity,
              'mode'   => self::EDIT_MODE_CLOSED,
            ];

            $widget_state_keys =
              array_slice($old_keys, 0, count($old_keys) - 2);

            if (!NestedArray::getValue($new_field_storage, $widget_state_keys)) {
              NestedArray::setValue(
                $new_field_storage,
                $widget_state_keys,
                ['media' => []]
              );
            }
          }

          // Ensure the referenced media file will be saved.
          $wrapped_media = $item_state['wrapper'];
          assert($wrapped_media instanceof MediaItemAdapterInterface);

          NestedArray::setValue($new_field_storage, $new_keys, $item_state);
        }
      }
    };

    // Recalculate original deltas.
    $recalculate_original_deltas = function ($field_storage, ContentEntityInterface $parent_entity) use (&$recalculate_original_deltas) {
      if (isset($field_storage['#fields'])) {
        foreach ($field_storage['#fields'] as $field_name => $widget_state) {
          if (isset($widget_state['media'])) {
            // If the parent field does not exist but we have media files in
            // widget state, something went wrong and we have a mismatch.
            // Throw an exception.
            if (!$parent_entity->hasField($field_name) &&
                !empty($widget_state['media'])) {
              throw new \LogicException(
                sprintf(
                  "Reordering media files resulted in a media file on non-existing field %d on parent entity %s/%s",
                  $field_name,
                  $parent_entity->getEntityTypeId(),
                  $parent_entity->id()
                )
              );
            }

            // Sort the media files by key so that they will be assigned to
            // the entity in the right order. Reset the deltas.
            ksort($widget_state['media']);
            $widget_state['media'] = array_values($widget_state['media']);

            $media_count     = count($widget_state['media']);
            $original_deltas = range(0, $media_count - 1);

            $field                    =& $field_storage['#fields'][$field_name];
            $field['original_deltas'] = $original_deltas;
            $field['items_count']     = $media_count;
            $field['real_item_count'] = $media_count;

            // Update the parent entity and point to the new children, if the
            // parent field does not exist, we also have no media files, so
            // we can just skip this, this is a dead leaf after re-ordering.
            if ($parent_entity->hasField($field_name)) {
              $parent_entity->set(
                $field_name,
                array_column($widget_state['media'], 'entity')
              );

              // Next process that field recursively.
              foreach (array_keys($widget_state['media']) as $delta) {
                if (isset($field_storage[$field_name][$delta]['subform'])) {
                  $field_storage[$field_name][$delta]['subform'] =
                    $recalculate_original_deltas(
                      $field_storage[$field_name][$delta]['subform'],
                      $parent_entity->get($field_name)->get($delta)->entity
                    );
                }
              }
            }
          }
        }
      }

      return $field_storage;
    };

    $parent_entity = $form_state->getFormObject()->getEntity();

    $new_field_storage =
      $recalculate_original_deltas($new_field_storage, $parent_entity);

    $form_state->set(['field_storage', '#parents'], $new_field_storage);
  }

  /**
   * Determine if this widget should be in translation mode.
   *
   * Initializes $this->isTranslating.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $host_entity
   *   The entity containing the field that this widget corresponds to.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   */
  protected function detectTranslationMode(
                                        ContentEntityInterface $host_entity,
                                        FormStateInterface $form_state): void {
    if ($this->isTranslating != NULL) {
      return;
    }

    $this->isTranslating = FALSE;

    if (!($host_entity instanceof TranslationStatusInterface) ||
        !$host_entity->isTranslatable()) {
      return;
    }

    if (!$host_entity->getEntityType()->hasKey('default_langcode')) {
      return;
    }

    $default_langcode_key =
      $host_entity->getEntityType()->getKey('default_langcode');

    if (!$host_entity->hasField($default_langcode_key)) {
      return;
    }

    // Supporting
    // \Drupal\content_translation\Controller\ContentTranslationController.
    if (!empty($form_state->get('content_translation'))) {
      // Adding a translation.
      $this->isTranslating = TRUE;
    }

    $langcode = $form_state->get('langcode');

    if ($host_entity->hasTranslation($langcode) &&
        $host_entity->getTranslation($langcode)->get($default_langcode_key)->value == 0) {
      // Editing a translation.
      $this->isTranslating = TRUE;
    }
  }

  /**
   * Determines whether to force-close a media file during translation.
   *
   * If untranslatable fields are hidden while translating, we are translating
   * the parent, and a media file is open, then close the media file if it does
   * not have translatable fields.
   *
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media entity being examined.
   *
   * @return bool
   *   TRUE if the media file should be closed for translation; or, FALSE,
   *   otherwise.
   */
  protected function shouldForceCloseForTranslation(
                               MediaItemAdapterInterface $wrapped_media): bool {
    $translating_force_close = FALSE;

    if ($this->hasTranslationManager()) {
      $settings =
        $this->translationManager->getBundleTranslationSettings(
          'media',
          $wrapped_media->getMediaType()
        );

      if (!empty($settings['untranslatable_fields_hide']) &&
          $this->isTranslating) {
        $translating_force_close = TRUE;

        $media_entity = $wrapped_media->toMediaEntity();

        $form_display   = $this->getFormDisplayMode($wrapped_media);
        $exposed_fields = array_keys($form_display->get('content'));

        // Check if the media file has translatable fields.
        foreach ($exposed_fields as $field) {
          if ($media_entity->hasField($field)) {
            $field_definition =
              $media_entity->get($field)->getFieldDefinition();

            $target_entity_type_id = $field_definition->getType();

            $target_bundle_id =
              $field_definition->getSetting('target_type');

            // Check if we are referencing media files.
            $is_media_reference =
              ($target_entity_type_id == 'entity_reference' &&
               $target_bundle_id == 'media');

            if ($is_media_reference || $field_definition->isTranslatable()) {
              $translating_force_close = FALSE;
              break;
            }
          }
        }
      }
    }

    return $translating_force_close;
  }

  /**
   * Builds a button for opening a specific media file up for editing.
   *
   * @param string $field_name
   *   The machine name of the field.
   * @param array $widget_state
   *   The state of all of the widgets of this field.
   * @param int $delta
   *   The index of the value that the widget is being constructed for.
   * @param string $id_prefix
   *   Unique prefix to use for buttons of the specific media file widget for
   *   which the button is being rendered.
   * @param string[] $parents
   *   The machine name of each of the form parents of the button.
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media file for which the button is being constructed.
   *
   * @return array
   *   Render array for the new button.
   */
  protected function buildEditButton(
                              string $field_name,
                              array $widget_state,
                              int $delta,
                              string $id_prefix,
                              array $parents,
                              MediaItemAdapterInterface $wrapped_media): array {
    $can_modify = $this->canModify($wrapped_media);

    return $this->buildActionButton([
      '#type'                    => 'submit',
      '#value'                   => $this->t('Edit'),
      '#name'                    => $id_prefix . '_edit',
      '#weight'                  => 1,
      '#submit'                  => [[static::class, 'mediaFileItemSubmit']],
      '#delta'                   => $delta,
      '#access'                  => $can_modify,
      '#edit_mode'               => self::EDIT_MODE_OPEN,

      '#attributes' => [
        'class' => [
          'inline-media-form-icon-button',
          'inline-media-form-icon-button-edit',
          'button--extrasmall',
        ],
        'title' => $this->t('Edit'),
      ],

      '#limit_validation_errors' => [
        array_merge($parents, [$field_name, $delta]),
      ],

      '#ajax' => [
        'callback' => [static::class, 'handleSingleItemAjax'],
        'wrapper'  => $widget_state['ajax_wrapper_id'],
      ],
    ]);
  }

  /**
   * Builds a button for collapsing a media file that is open for editing.
   *
   * @param string $field_name
   *   The machine name of the field.
   * @param array $widget_state
   *   The state of all of the widgets of this field.
   * @param int $delta
   *   The index of the value that the widget is being constructed for.
   * @param string $id_prefix
   *   Unique prefix to use for buttons of the specific media file widget for
   *   which the button is being rendered.
   * @param string[] $parents
   *   The machine name of each of the form parents of the button.
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media file for which the button is being constructed.
   *
   * @return array
   *   Render array for the new button.
   */
  protected function buildCollapseButton(
                              string $field_name,
                              array $widget_state,
                              int $delta,
                              string $id_prefix,
                              array $parents,
                              MediaItemAdapterInterface $wrapped_media): array {
    $can_modify = $this->canModify($wrapped_media);

    return [
      '#value'                   => $this->t('Collapse'),
      '#name'                    => $id_prefix . '_collapse',
      '#weight'                  => 1,
      '#submit'                  => [[static::class, 'mediaFileItemSubmit']],
      '#access'                  => $can_modify,
      '#edit_mode'               => self::EDIT_MODE_CLOSED,
      '#delta'                   => $delta,
      '#attributes'              => [
        'class' => [
          'inline-media-form-icon-button',
          'inline-media-form-icon-button-collapse',
          'button--extrasmall',
        ],
        'title' => $this->t('Collapse'),
      ],

      '#limit_validation_errors' => [
        array_merge($parents, [$field_name, $delta]),
      ],

      '#ajax'                    => [
        'callback' => [static::class, 'handleSingleItemAjax'],
        'wrapper'  => $widget_state['ajax_wrapper_id'],
      ],
    ];
  }

  /**
   * Builds a button for removing a reference to a specific media file.
   *
   * @param array $widget_state
   *   The state of all of the widgets of this field.
   * @param string $id_prefix
   *   Unique prefix to use for buttons of the specific media file widget for
   *   which the button is being rendered.
   * @param int $delta
   *   The index of the value that the widget is being constructed for.
   *
   * @return array
   *   Render array for the new button.
   */
  protected function buildRemoveButton(array $widget_state,
                                       string $id_prefix,
                                       int $delta): array {
    return [
      '#type'                    => 'submit',
      '#value'                   => $this->t('Remove'),
      '#name'                    => $id_prefix . '_remove',
      '#weight'                  => 501,
      '#submit'                  => [[static::class, 'mediaFileItemSubmit']],
      '#delta'                   => $delta,
      '#edit_mode'               => self::EDIT_MODE_REMOVED,
      '#attributes'              => [
        'class' => ['button--small'],
      ],

      // Ignore all validation errors because deleting invalid media files
      // is allowed.
      '#limit_validation_errors' => [],

      '#ajax'                    => [
        'callback' => [static::class, 'handleSingleItemAjax'],
        'wrapper'  => $widget_state['ajax_wrapper_id'],
      ],
    ];
  }

  /**
   * Builds a button for collapsing all open media files.
   *
   * @param string $field_name
   *   The machine name of the field.
   * @param string $id_prefix
   *   Unique prefix to use for buttons of the specific media file widget for
   *   which the button is being rendered.
   *
   * @return array
   *   Render array for the new button.
   */
  protected function buildCollapseAllButton(string $field_name,
                                            string $id_prefix): array {
    $submitCallback = [[static::class, 'changeAllEditModeSubmit']];

    return $this->buildActionButton([
      '#type'                    => 'submit',
      '#value'                   => $this->t('Collapse all'),
      '#submit'                  => $submitCallback,
      '#name'                    => $id_prefix . '_collapse_all',
      '#weight'                  => -1,
      '#edit_mode'               => self::EDIT_MODE_CLOSED,
      '#attributes'              => [
        'class' => [
          'inline-media-form-icon-button',
          'inline-media-form-icon-button-collapse',
          'button--extrasmall',
        ],

        'title' => $this->t('Collapse all'),
      ],
      '#limit_validation_errors' => [
        array_merge(
          $this->fieldParents,
          [$field_name]
        ),
      ],
      '#ajax'                    => [
        'callback' => [static::class, 'handleAllItemAjax'],
        'wrapper'  => $this->fieldWrapperId,
      ],
    ]);
  }

  /**
   * Builds a button for bulk renaming all media files.
   *
   * @param string $field_name
   *   The machine name of the field.
   * @param string $id_prefix
   *   Unique prefix to use for buttons of the specific media file widget for
   *   which the button is being rendered.
   *
   * @return array
   *   Render array for the new button.
   */
  protected function buildBulkRenameButton(string $field_name,
                                           string $id_prefix): array {

    $submitCallback = [[static::class, 'showBulkRenameToolbarSubmit']];

    return $this->buildActionButton([
      '#type'                    => 'submit',
      '#value'                   => $this->t('Bulk rename'),
      '#name'                    => $id_prefix . '_bulk_rename',
      '#submit'                  => $submitCallback,
      '#attributes'              => [
        'class' => [
          'inline-media-form-icon-button',
          'inline-media-form-icon-button-bulk-rename',
          'button--extrasmall',
        ],
        'title' => $this->t('Bulk rename'),
      ],
      '#limit_validation_errors' => [
        array_merge(
          $this->fieldParents,
          [$field_name]
        ),
      ],
      '#ajax'                    => [
        'callback' => [static::class, 'handleAllItemAjax'],
        'wrapper'  => $this->fieldWrapperId,
      ],
    ]);
  }

  /**
   * Builds a button for opening up all open media files for editing.
   *
   * @param string $id_prefix
   *   Unique prefix to use for buttons of the specific media file widget for
   *   which the button is being rendered.
   *
   * @return array
   *   Render array for the new button.
   */
  protected function buildEditAllButton(string $id_prefix): array {
    $submitCallback = [[static::class, 'changeAllEditModeSubmit']];

    return $this->buildActionButton([
      '#type'                    => 'submit',
      '#value'                   => $this->t('Edit all'),
      '#submit'                  => $submitCallback,
      '#name'                    => $id_prefix . '_edit-all',
      '#edit_mode'               => self::EDIT_MODE_OPEN,
      '#attributes'              => [
        'class' => [
          'inline-media-form-icon-button',
          'inline-media-form-icon-button-edit',
          'button--extrasmall',
        ],

        'title' => $this->t('Edit all'),
      ],
      '#limit_validation_errors' => [],
      '#ajax'                    => [
        'callback' => [static::class, 'handleAllItemAjax'],
        'wrapper'  => $this->fieldWrapperId,
      ],
    ]);
  }

  /**
   * Builds the widget for editing a single media file.
   *
   * @param array $element
   *   The base widget form element render array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   * @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display
   *   The form display to use for editing the media file.
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media file for which a widget is being constructed.
   *
   * @return array
   *   A render array for the open editing widget.
   */
  protected function buildOpenWidget(
                              array $element,
                              FormStateInterface $form_state,
                              EntityFormDisplayInterface $display,
                              MediaItemAdapterInterface $wrapped_media): array {
    $has_view_access = $this->canView($wrapped_media);
    $media_entity    = $wrapped_media->toMediaEntity();

    $display->buildForm($media_entity, $element['subform'], $form_state);

    $hide_untranslatable_fields =
      $media_entity->isDefaultTranslationAffectedOnly();

    $summary = $wrapped_media->toFieldSummaries($display->getMode());

    if (!empty($summary)) {
      $element['top']['summary']['fields_info'] = [
        '#theme'    => 'inline_media_form_summary',
        '#expanded' => TRUE,
        '#access'   => $has_view_access,
        '#summary'  => [
          'content' => $summary,
        ],
      ];
    }

    foreach (Element::children($element['subform']) as $field) {
      if ($media_entity->hasField($field)) {
        $field_definition =
          $media_entity->get($field)->getFieldDefinition();

        // Do a check if we have to add a class to the form element. We need
        // the class to show and hide elements, depending of the active
        // perspective. We need them to filter out entity reference fields that
        // reference media files, cause otherwise we have problems with showing
        // and hiding the right fields in nested media files.
        if (($field_definition->getType() == 'entity_reference') &&
            ($field_definition->getSetting('target_type') == 'media')) {
          $is_media_field = TRUE;
        }
        else {
          $is_media_field = FALSE;
        }

        if (!$is_media_field) {
          $element['subform'][$field]['#attributes']['class'][] =
            'inline-media-form-content';

          $element['top']['summary']['fields_info'] = [
            '#theme'    => 'inline_media_form_summary',
            '#expanded' => TRUE,
            '#access'   => $has_view_access,
            '#summary'  => [
              'content' => $summary,
            ],
          ];
        }

        $translatable = $field_definition->isTranslatable();

        // Hide untranslatable fields when configured to do so except
        // media file fields.
        if (!$translatable && $this->isTranslating && !$is_media_field) {
          if ($hide_untranslatable_fields) {
            $element['subform'][$field]['#access'] = FALSE;
          }
          else {
            $element['subform'][$field]['widget']['#after_build'][] =
              [static::class, 'addTranslatabilityClue'];
          }
        }
      }
    }

    // Remove the revision log message widget. We populate it automatically from
    // the log message of the parent entity. Using '#access' => FALSE is
    // insufficient, because the hidden form element will still load the log
    // message from the last revision of this media file, and it will override
    // the value we will set in copyRevisionMetadata().
    unset($element['subform']['revision_log_message']);

    return $element;
  }

  /**
   * Builds the widget for previewing a media file that is not open for editing.
   *
   * @param array $element
   *   The base widget form element render array.
   * @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display
   *   The form display to use for editing the media file.
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media file for which a widget is being constructed.
   *
   * @return array
   *   A render array for the closed/summary widget.
   */
  protected function buildClosedWidget(
                              array $element,
                              EntityFormDisplayInterface $display,
                              MediaItemAdapterInterface $wrapped_media): array {
    $element['subform'] = [];

    $summary = $wrapped_media->toFieldSummaries($display->getMode());

    if (!empty($summary)) {
      $element['top']['summary']['fields_info'] = [
        '#theme'    => 'inline_media_form_summary',
        '#expanded' => FALSE,
        '#summary'  => [
          'content' => $summary,
        ],
      ];
    }

    return $element;
  }

  /**
   * Applies a workaround for rendering field groups in media file edit forms.
   *
   * This workaround should no longer be necessary after #2640056.
   *
   * @param array $element
   *   The base widget form element render array.
   * @param \Drupal\media\MediaInterface $media_entity
   *   The media file for which a widget is being constructed.
   * @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display
   *   The form display being used to present the media file.
   *
   * @return array
   *   The modified widget render array.
   */
  protected function applyFieldGroupWorkaroundForIssue2640056(
                              array $element,
                              MediaInterface $media_entity,
                              EntityFormDisplayInterface $form_display): array {
    $context = [
      'entity_type'     => $media_entity->getEntityTypeId(),
      'bundle'          => $media_entity->bundle(),
      'entity'          => $media_entity,
      'context'         => 'form',
      'display_context' => 'form',
      'mode'            => $form_display->getMode(),
    ];

    field_group_attach_groups($element['subform'], $context);

    if (method_exists(FormatterHelper::class, 'formProcess')) {
      $element['subform']['#process'][] =
        [FormatterHelper::class, 'formProcess'];
    }
    elseif (function_exists('field_group_form_pre_render')) {
      $element['subform']['#pre_render'][] = 'field_group_form_pre_render';
    }
    elseif (function_exists('field_group_form_process')) {
      $element['subform']['#process'][] = 'field_group_form_process';
    }

    return $element;
  }

  /**
   * Invokes hook_imf_widget_header_actions_alter().
   *
   * @param array $form
   *   The render array for the entity form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity form.
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   All of the deltas/distinct values of the field.
   * @param array $element
   *   The form element render array containing the basic properties for the
   *   top-level widget.
   * @param array $header_actions
   *   A reference to the contents of the header actions:
   *     - actions - The default actions, which are always visible.
   *     - dropdown_actions - Actions that appear in the drop-down
   *       sub-component.
   *     - toolbar - The toolbar for providing input to the active tool (e.g.,
   *       bulk rename).
   */
  protected function alterWidgetHeaderActions(
                                       array $form,
                                       FormStateInterface $form_state,
                                       FieldItemListInterface $items,
                                       array $element,
                                       array &$header_actions): void {
    $original_widget_state =
      $this->getCurrentWidgetState($form_state, $this->fieldParents);

    $context = [
      'form'                    => $form,
      'widget'                  => $original_widget_state,
      'items'                   => $items,
      'element'                 => $element,
      'form_state'              => $form_state,
      'is_translating'          => $this->isTranslating,
    ];

    // Allow modules to alter widget actions.
    $this->moduleHandler->alter(
      'imf_widget_header_actions',
      $header_actions,
      $context
    );
  }

  /**
   * Invokes hook_imf_widget_delta_actions_alter().
   *
   * @param array $form
   *   The render array for the entity form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity form.
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   All of the deltas/distinct values of the field.
   * @param int $delta
   *   The order/ordinal position of this item in the array of items (0, 1, 2,
   *   etc).
   * @param array $element
   *   The form element render array containing the basic properties for the
   *   widget.
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The media item adapter wrapper around the media file for this widget.
   * @param array $widget_actions
   *   A reference to the array of 'actions' and 'dropdown_actions':
   *     - actions - The default actions, which are always visible.
   *     - dropdown_actions - Actions that appear in the drop-down
   *       sub-component.
   */
  protected function alterWidgetDeltaActions(
                                       array $form,
                                       FormStateInterface $form_state,
                                       FieldItemListInterface $items,
                                       int $delta,
                                       array $element,
                                       MediaItemAdapterInterface $wrapped_media,
                                       array &$widget_actions): void {
    $parents = $element['#field_parents'];

    $original_widget_state =
      $this->getCurrentWidgetState($form_state, $parents);

    $context = [
      'form'                    => $form,
      'widget'                  => $original_widget_state,
      'items'                   => $items,
      'delta'                   => $delta,
      'element'                 => $element,
      'form_state'              => $form_state,
      'wrapper'                 => $wrapped_media,
      'is_translating'          => $this->isTranslating,
      'allow_reference_changes' => $this->allowReferenceChanges(),
    ];

    // Allow modules to alter widget actions.
    $this->moduleHandler->alter(
      'imf_widget_delta_actions',
      $widget_actions,
      $context
    );
  }

  /**
   * Gets the IDs of all media entities currently referenced by the widget.
   *
   * @param array $widget_state
   *   The current widget state.
   *
   * @return int[]
   *   The IDs of all media entities that the field references.
   */
  protected function getReferencedMediaIds(array $widget_state): array {
    return array_reduce(
      $widget_state['media'],
      function (array $referenced_ids, array $widget_delta_state) {
        $entity = $widget_delta_state['entity'] ?? NULL;
        $mode   = $widget_delta_state['mode'] ?? NULL;

        assert(($entity !== NULL) && ($entity instanceof EntityInterface));

        if ($mode !== self::EDIT_MODE_REMOVED) {
          $referenced_ids[] = $entity->id();
        }

        return $referenced_ids;
      },
      []
    );
  }

  /**
   * Massages form values of a delta into the format expected for field values.
   *
   * @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display
   *   The form display being used for editing this delta.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param array $delta_element
   *   The widget form element for this delta.
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The adapter around the media file for this delta.
   * @param array $value
   *   The submitted form value for this delta, as produced by the widget.
   * @param string $edit_mode
   *   The edit mode of the widget for this particular delta.
   *
   * @return array
   *   The field values to use for this delta.
   */
  protected function massageDeltaFormValues(
                                      EntityFormDisplayInterface $display,
                                      FormStateInterface $form_state,
                                      array $delta_element,
                                      MediaItemAdapterInterface $wrapped_media,
                                      array $value,
                                      string $edit_mode): array {
    $this->copyRevisionMetadata($form_state, $wrapped_media);

    if ($edit_mode == self::EDIT_MODE_REMOVED) {
      // If our mode is "removed", don't save or reference this entity.
      $value['target_id'] = NULL;
    }
    else {
      $media_entity = $wrapped_media->toMediaEntity();

      if ($edit_mode == self::EDIT_MODE_OPEN) {
        $display->extractFormValues(
          $media_entity,
          $delta_element['subform'],
          $form_state
        );
      }

      // A content entity form saves without any rebuild. It needs to set the
      // language to update it in case of language change.
      $langcode_key  = $media_entity->getEntityType()->getKey('langcode');
      $form_langcode = $form_state->get('langcode');

      if ($media_entity->get($langcode_key)->value != $form_langcode) {
        // If a translation in the given language already exists, switch to
        // that. If there is none yet, update the language.
        if ($media_entity->hasTranslation($form_langcode)) {
          $media_entity = $media_entity->getTranslation($form_langcode);
        }
        else {
          $media_entity->set($langcode_key, $form_langcode);
        }

        $wrapped_media = MediaItemAdapter::create($media_entity);
      }

      // We can only use the entity form display to display validation errors
      // if it is in edit mode.
      if (!$form_state->isValidationComplete()) {
        if ($edit_mode === self::EDIT_MODE_OPEN) {
          $display->validateFormValues(
            $media_entity,
            $delta_element['subform'],
            $form_state
          );
        }
        elseif ($form_state->getLimitValidationErrors() === NULL) {
          // Assume that the entity is being saved/previewed, in this case,
          // validate even the closed media files. If there are validation
          // errors, add them on the parent level. Validation errors do not
          // rebuild the form so it's not possible to un-collapse the form
          // at this point.
          $violations = $media_entity->validate();
          $violations->filterByFieldAccess();

          if (!empty($violations)) {
            /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
            foreach ($violations as $violation) {
              // Ignore text format related validation errors by ignoring
              // the .format property.
              if (substr($violation->getPropertyPath(), -7) === '.format') {
                continue;
              }

              $form_state->setError(
                $delta_element,
                $this->t('Validation error on collapsed media file @path: @message',
                  [
                    '@path'    => $violation->getPropertyPath(),
                    '@message' => $violation->getMessage(),
                  ]
                )
              );
            }
          }
        }
      }

      $value['wrapper']            = $wrapped_media;
      $value['entity']             = $wrapped_media->toMediaEntity();
      $value['target_id']          = $media_entity->id();
      $value['target_revision_id'] = $media_entity->getRevisionId();
    }

    return $value;
  }

  /**
   * Copies the revision flag and revision log message from the parent entity.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the entity edit form.
   * @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
   *   The adapter around the media file receiving the revision info.
   */
  protected function copyRevisionMetadata(
                              FormStateInterface $form_state,
                              MediaItemAdapterInterface $wrapped_media): void {
    // Only create a new revision if we actually have changes.
    if ($wrapped_media->isSaveNeeded()) {
      $media_entity = $wrapped_media->toMediaEntity();

      $create_revision     = $form_state->getValue('revision') ?? FALSE;
      $revision_log_values = $form_state->getValue('revision_log') ?? [];

      $revision_message =
        NestedArray::getValue($revision_log_values, [0, 'value']);

      $media_entity->setNewRevision($create_revision);
      $media_entity->setRevisionCreationTime($this->time->getRequestTime());
      $media_entity->setRevisionUserId($this->user->id());

      if (empty($revision_message)) {
        $media_entity->setRevisionLogMessage(NULL);
      }
      else {
        $media_entity->setRevisionLogMessage($revision_message);
      }
    }
  }

  /**
   * Builds the state needed to open the media library.
   *
   * @param array $opener_state
   *   The state of the media library modal.
   * @param string $build_id
   *   The build ID of the entity edit form array.
   *
   * @return \Drupal\media_library\MediaLibraryState
   *   The state to use when opening the media library.
   */
  protected static function buildMediaLibraryState(array $opener_state,
                                                   string $build_id): MediaLibraryState {
    $host_entity            = $opener_state['host_entity'];
    $field_widget_id        = $opener_state['field_widget_id'];
    $field_name             = $opener_state['field_name'];
    $referenced_entity_ids  = $opener_state['referenced_entity_ids'];
    $allowed_media_type_ids = $opener_state['allowed_media_type_ids'];
    $remaining_count        = $opener_state['remaining_count'];
    $library_view_display   = $opener_state['library_view_display'];

    $host_entity_type = $host_entity->getEntityType();

    // This particular media library opener needs some extra metadata for its
    // \Drupal\media_library\MediaLibraryOpenerInterface::getSelectionResponse()
    // to be able to target the element whose 'data-media-library-widget-value'
    // attribute is the same as $field_widget_id. The entity ID, entity type ID,
    // bundle, field name are used for access checking.
    $opener_parameters = [
      'field_widget_type' => static::class,
      'field_widget_id'   => $field_widget_id,
      'entity_type_id'    => $host_entity_type->id(),
      'bundle'            => $host_entity->bundle(),
      'field_name'        => $field_name,
    ];

    if (!empty($referenced_entity_ids)) {
      /** @var \Drupal\Core\TempStore\PrivateTempStoreFactory */
      $temp_store_factory = \Drupal::service('tempstore.private');

      // Store the referenced IDs in a temp store to avoid cluttering up the
      // query string. This also avoids a problem where certain CDNs like Fastly
      // will not serve very long URLs.
      //
      // https://docs.fastly.com/en/guides/resource-limits#request-and-response-limits
      $temp_store_id = 'media_library_opener_' . $build_id;
      $temp_store    = $temp_store_factory->get($temp_store_id);

      $temp_store->set('current_ids_selected', $referenced_entity_ids);

      $opener_parameters['selected_id_store_id'] = $temp_store_id;
    }

    // Only add the entity ID when we actually have one. The entity ID needs to
    // be a string to ensure that the media library state generates its
    // tamper-proof hash in a consistent way.
    if (!$host_entity->isNew()) {
      $opener_parameters['entity_id'] = (string) $host_entity->id();

      if ($host_entity_type->isRevisionable()) {
        assert($host_entity instanceof RevisionableInterface);

        $opener_parameters['revision_id'] =
          (string) $host_entity->getRevisionId();
      }
    }

    $initial_media_type_id = reset($allowed_media_type_ids);

    if (self::doesMediaLibrarySupportCustomViewDisplay()) {
      [$library_view, $library_display] = explode('.', $library_view_display);

      $state = MediaLibraryState::create(
        'media_library.opener.field_widget',
        $allowed_media_type_ids,
        $initial_media_type_id,
        $remaining_count,
        $opener_parameters,
        $library_view,
        $library_display
      );
    }
    else {
      $state = MediaLibraryState::create(
        'media_library.opener.field_widget',
        $allowed_media_type_ids,
        $initial_media_type_id,
        $remaining_count,
        $opener_parameters
      );
    }

    return $state;
  }

}

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

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