custom_elements-8.x-2.x-dev/modules/custom_elements_ui/src/Form/EntityCustomElementsDisplayEditForm.php

modules/custom_elements_ui/src/Form/EntityCustomElementsDisplayEditForm.php
<?php

namespace Drupal\custom_elements_ui\Form;

use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\PluginSettingsInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RedirectDestinationTrait;
use Drupal\Core\Url;
use Drupal\custom_elements\CustomElementGeneratorTrait;
use Drupal\custom_elements\CustomElementsFieldFormatterInterface;
use Drupal\custom_elements\CustomElementsFieldFormatterPluginManager;
use Drupal\custom_elements\PreviewProvider\CustomElementsPreviewResolver;
use Drupal\field_ui\FieldUI;
use Drupal\field_ui\Form\EntityDisplayFormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\custom_elements\Entity\EntityCeDisplayInterface;

/**
 * Forked version of EntityDisplayFormBase class of core.
 *
 * The EntityCeDisplay object has makes use of CeFormatters instead of core
 * field formatters for rendering display components - as returned by
 * EntityCeDisplay::getRenderer(). Our CE-field-formatters are properly
 * configurable plugins. So instead of having plugin-settings forms like
 * core-field formatters, we have plugin configuration forms. The existing
 * EntityDisplay logic has been adapted to the new API, some keys, variable
 * names and comments still refer to "plugin settings" instead of "plugin
 * configuration" though.
 * This is good to keep, because we are also supporting the configuration of
 * core field formatters as part of our CoreFieldCeFieldFormatter plugins.
 *
 * However, the core field formatter settings form API is simple and pre-dates
 * the core SubformState, so it needs to have the same global location in the
 * form array as core does it. Long story short, we need to keep the
 * 'settings_edit_form' key there. For example,
 * empty_fields_field_formatter_third_party_settings_form() relies on it.
 *
 * @internal
 *   Form classes are internal.
 */
class EntityCustomElementsDisplayEditForm extends EntityDisplayFormBase {

  use CustomElementGeneratorTrait;
  use RedirectDestinationTrait;

  /**
   * {@inheritdoc}
   */
  protected $displayContext = 'view';

  /**
   * The custom element formatter plugin manager.
   *
   * @var \Drupal\custom_elements\CustomElementsFieldFormatterPluginManager
   */
  protected $customElementFormatterPluginManager;

  /**
   * The custom elements display preview resolver service.
   *
   * @var \Drupal\custom_elements\PreviewProvider\CustomElementsPreviewResolver
   */
  protected $previewResolver;

  /**
   * The renderer service.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected RendererInterface $renderer;

  /**
   * Constructs a new EntityCustomElementsDisplayEditForm.
   *
   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
   *   The field type manager.
   * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager
   *   The widget or formatter plugin manager.
   * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
   *   (optional) The entity display_repository.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   (optional) The entity field manager.
   * @param \Drupal\custom_elements\CustomElementsFieldFormatterPluginManager $ce_formatter_plugin_manager
   *   The custom element formatter plugin manager.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer service.
   * @param \Drupal\custom_elements\PreviewProvider\CustomElementsPreviewResolver $preview_resolver
   *   The preview resolver service.
   */
  public function __construct(
    FieldTypePluginManagerInterface $field_type_manager,
    PluginManagerBase $plugin_manager,
    EntityDisplayRepositoryInterface $entity_display_repository,
    EntityFieldManagerInterface $entity_field_manager,
    CustomElementsFieldFormatterPluginManager $ce_formatter_plugin_manager,
    RendererInterface $renderer,
    CustomElementsPreviewResolver $preview_resolver,
  ) {
    parent::__construct($field_type_manager, $plugin_manager, $entity_display_repository, $entity_field_manager);
    $this->customElementFormatterPluginManager = $ce_formatter_plugin_manager;
    $this->renderer = $renderer;
    $this->previewResolver = $preview_resolver;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    // @phpstan-ignore-next-line
    return new static(
      $container->get('plugin.manager.field.field_type'),
      $container->get('plugin.manager.field.formatter'),
      $container->get('entity_display.repository'),
      $container->get('entity_field.manager'),
      $container->get('custom_elements.plugin.manager.field.custom_element_formatter'),
      $container->get('renderer'),
      $container->get('custom_elements.preview_resolver')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function setEntity(EntityInterface $entity) {
    assert($entity instanceof EntityCeDisplayInterface);
    // Convert loaded entity's components to the 'fixed row names' which the
    // form can use throughout AJAX reloads. Add fake component properties that
    // are unset on save():
    // - 'name' for the editable names,
    // - 'region' for compatibility with Core tabledrag.js (always 'content')
    $new_components = $skip_components = $broken_components = [];
    $remove_components = array_fill_keys(array_keys($entity->getComponents()), TRUE);
    foreach ($entity->getComponents() as $component_name => $component) {
      if (!isset($component['field_name'])) {
        $broken_components[$component_name] = TRUE;
        continue;
      }
      if (isset($component['name'])) {
        // This is an already-converted entity (set by e.g. an AJAX call).
        $skip_components[$component_name] = TRUE;
        unset($remove_components[$component_name]);
        continue;
      }
      $component['name'] = $component_name;
      $new_components[$component['field_name']] = $component;
    }

    if ($broken_components) {
      $this->messenger()->addWarning($this->t("Some fields in the custom elements display are not shown and will be lost, because they have no 'field_name' property: @keys", ['@keys' => implode(', ', array_keys($broken_components))]));
    }

    if ($new_components) {
      if ($skip_components) {
        // Either all, or none, of the components should already have 'name'
        // set. A combination of both points to a bug / wrongly saved entity.
        // But do not log; the form will correct this on save. Only if one
        // component's 'name' clashes with another's key, warn that it's being
        // overwritten.
        $overwritten_components = array_intersect_key($skip_components, $new_components);
        if ($overwritten_components) {
          $this->messenger()->addWarning($this->t("Some fields in the custom elements display are not shown and will be lost, because they clash with other components' names: @keys", ['@keys' => implode(', ', array_keys($skip_components))]));
        }
      }

      // Key the changed components by the form row names; remove unused keys.
      foreach ($new_components as $row_name => $component) {
        $entity->setComponent($row_name, $component);
        unset($remove_components[$row_name]);
      }
      foreach (array_keys($remove_components) as $key) {
        $entity->removeComponent($key);
      }
    }

    return parent::setEntity($entity);
  }

  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\custom_elements\Entity\EntityCeDisplayInterface $entity */
    $entity = $this->getEntity();

    [$show_lb_option, $lb_extra_message, $layout_link_view_mode, $always_show_non_lb_settings]
      = $this->layoutBuilderEnabledOptions();

    if (!$show_lb_option && $lb_extra_message) {
      // General message, when "use LB" is not applicable / invisible.
      $form['header_message'] = [
        '#markup' => '<p><em>' . $lb_extra_message . '</em></p>',
      ];
    }

    $form['custom_element_name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Custom element name'),
      '#description' => $this->t('Defines the custom element which is going to be used for the display. There must be a matching implementation in the frontend to render the display.'),
      '#default_value' => $entity->getCustomElementName(),
      '#size' => 20,
      // We could make the CE-name not-required when force_auto=on, because
      // code can set their own CE-name/tag. However,
      // - an empty CE-name for a loaded entity always gets overwritten with a
      //   default value by EntityCeDisplay::init();
      // - the text element is always visible on screen, so saving an empty
      //   value will just make the user confused about why the value shows as
      //   non-empty after save.
      // So, just keep it required.
      '#required' => TRUE,
    ];

    // If Layout Builder isn't enabled, useLayoutBuilder is ignored, but if
    // this value is True then don't force-override to False on next save.
    $form['force_layout'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Use Layout Builder'),
      '#description' => $this->t("The custom element will consist of 'drupal-layout' elements, each representing a layout's section."),
      '#default_value' => $entity->getUseLayoutBuilder(),
      '#access' => (bool) $show_lb_option,
    ];
    if ($show_lb_option && $lb_extra_message) {
      $form['force_layout']['#description'] .= '<br>' . $lb_extra_message;
    }
    if ($layout_link_view_mode) {
      $entity_type = $this->entityTypeManager->getDefinition($entity->getTargetEntityTypeId());
      $bundle_parameter_key = $entity_type->getBundleEntityType() ?: 'bundle';
      $route_parameters = [
        $bundle_parameter_key => $entity->getTargetBundle(),
        'view_mode_name' => $layout_link_view_mode,
      ];
      $form['manage_layout_wrapper'] = [
        '#type' => 'fieldset',
        '#states' => [
          'visible' => [
            ':input[name="force_layout"]' => ['checked' => TRUE],
          ],
        ],
      ];
      $form['manage_layout_wrapper']['manage_layout'] = [
        '#type' => 'link',
        '#title' => $this->t('Manage layout'),
        '#attributes' => ['class' => ['button']],
        '#url' => Url::fromRoute("layout_builder.defaults." . $entity->getTargetEntityTypeId() . '.view', $route_parameters, ['query' => $this->getDestinationArray()]),
      ];
      // If "Use layout" is originally off, remind the user that it will
      // still be off if they click "Manage layout" now.
      $messages = [];
      if (!$entity->getUseLayoutBuilder()) {
        $messages[] = $this->t('(Save this screen first.)');
      }
      if ($layout_link_view_mode != $this->entity->getMode()) {
        $messages[] = $this->t('Note this is the layout for view mode %other_mode, not %this_mode.', [
          '%this_mode' => $this->entity->getMode(),
          '%other_mode' => $layout_link_view_mode,
        ]);
      }
      if ($show_lb_option && !$always_show_non_lb_settings) {
        // The field list cannot be hidden (because draggable-js stuff), which
        // makes it unclear that it's sometimes not used.
        $messages[] = $this->t("The below field table is not used when 'Use Layout Builder' or 'Automatic processing' are enabled.");
      }
      if ($messages) {
        $form['manage_layout_wrapper']['manage_layout_message'] = ['#markup' => implode('<br>', $messages)];
      }
    }

    // @todo replace by parent::form() when EntityDisplayFormBase is removed
    //   as parent.
    $form = EntityForm::form($form, $form_state);

    $field_definitions = $this->getConfiguredFieldDefinitions();

    if ($show_lb_option && !$always_show_non_lb_settings) {
      // The field list cannot be hidden (because draggable-js stuff), which
      // makes it unclear that it's sometimes not used.
      // This was added into the fieldset above, if possible.
      if (!$layout_link_view_mode) {
        $form['fields_usage_message_wrapper'] = [
          '#type' => 'fieldset',
          '#states' => [
            'visible' => [
              ':input[name="force_layout"]' => ['checked' => TRUE],
            ],
          ],
          'message' => [
            '#markup' => $this->t("The below field table is not used when 'Use Layout Builder' or 'Automatic processing' are enabled."),
          ],
        ];
      }
    }
    elseif ($entity->getForceAutoProcessing()) {
      // Since we moved the autoprocessing option to below the field table,
      // it's not clear that the field table is not used, especially if it
      // contains several fields. No need to make it invisible dynamically
      // when autoprocessing gets enabled.
      $form['fields_usage_message_wrapper'] = [
        '#markup' => '<p><em>' . $this->t("The below field table is not used when 'Automatic processing' is enabled.") . '</em></p>',
      ];
    }

    $form += [
      '#entity_type' => $this->entity->getTargetEntityTypeId(),
      '#bundle' => $this->entity->getTargetBundle(),
      '#fields' => array_keys($field_definitions),
    ];

    $regions = $this->getRegions();
    $table = [
      '#type' => 'field_ui_table',
      '#header' => $this->getTableHeader(),
      '#regions' => [
        'content' => $regions['content'],
      ],
      '#attributes' => [
        'class' => ['field-ui-overview'],
        'id' => 'field-display-overview',
      ],
      '#tabledrag' => [
        [
          'action' => 'order',
          'relationship' => 'sibling',
          'group' => 'field-weight',
        ],
        [
          'action' => 'match',
          'relationship' => 'parent',
          'group' => 'field-parent',
          'subgroup' => 'field-parent',
          'source' => 'field-name',
        ],
      ],
    ];

    // Build only fields configured in the display.
    foreach ($field_definitions as $field_name => $field_definition) {
      $table[$field_name] = $this->buildFieldRow($field_definition, $form, $form_state);
    }

    $trigger = $form_state->getTriggeringElement();
    if ($trigger && isset($trigger['#name']) && $trigger['#name'] == 'add_select') {
      // Add a new field.
      $field_name = $form_state->getValue('add_select');
      $field_definition = $this->getFieldDefinitions()[$field_name];
      // Component name is never saved; for the moment, it must be equal to the
      // field name.
      $component_name = $field_name;
      $default_name = str_starts_with($field_name, 'field_') ? substr($field_name, strlen('field_')) : $field_name;
      $this->entity->setComponent($component_name, [
        'region' => 'content',
        'name' => $default_name,
        'field_name' => $field_name,
        'is_slot' => str_starts_with($field_definition->getType(), 'text') ? 1 : 0,
        'formatter' => 'auto',
      ]);
      $table[$field_name] = $this->buildFieldRow($field_definition, $form, $form_state);

      $form['#fields'][] = $field_name;
    }

    $form['fields'] = $table;

    // Add new fields: for now, only the ones that aren't added yet.
    $add_options = [];
    $configured_fields_as_keys = array_flip($form['#fields']);
    foreach ($this->getFieldDefinitions() as $field_name => $field_definition) {
      if (!isset($configured_fields_as_keys[$field_name])) {
        $add_options[$field_name] = $field_definition->getLabel();
      }
    }
    if ($add_options) {
      asort($add_options);
      $form['add_select'] = [
        '#type' => 'select',
        '#title' => $this->t('Add field'),
        '#options' => $add_options,
        '#empty_option' => $this->t('- Select -'),
        '#ajax' => [
          'callback' => '::multistepAjax',
          'wrapper' => 'field-display-overview-wrapper',
          'effect' => 'fade',
        ],
      ];
    }

    // Advanced settings.
    $form['advanced'] = [
      '#type' => 'details',
      '#title' => $this->t('Advanced'),
      '#open' => $entity->getForceAutoProcessing(),
    ];

    $form['advanced']['force_auto'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Automatic processing'),
      '#description' => $this->t('Builds the custom element with code-controlled processing logic that varies by field type. This may override the custom element name. The field table above will not be used.'),
      '#default_value' => $entity->getForceAutoProcessing(),
    ];

    // Custom display settings.
    if ($this->entity->getMode() == 'default') {
      // Only show the settings if there is at least one custom display mode.
      $display_mode_options = $this->getDisplayModeOptions();
      // Unset default option.
      unset($display_mode_options['default']);
      if ($display_mode_options) {
        $form['modes'] = [
          '#type' => 'details',
          '#title' => $this->t('Custom display settings'),
        ];
        // Prepare default values for the 'Custom display settings' checkboxes.
        $default = [];
        if ($enabled_displays = array_filter($this->getDisplayStatuses())) {
          $default = array_keys(array_intersect_key($display_mode_options, $enabled_displays));
        }
        natcasesort($display_mode_options);
        $form['modes']['display_modes_custom'] = [
          '#type' => 'checkboxes',
          '#title' => $this->t('Use custom display settings for the following @display_context modes', ['@display_context' => $this->displayContext]),
          '#options' => $display_mode_options,
          '#default_value' => $default,
        ];
        // Provide link to manage display modes.
        $form['modes']['display_modes_link'] = $this->getDisplayModesLink();
      }
    }
    // Custom elements display preview.
    $form['preview'] = [
      '#type' => 'details',
      '#title' => $this->t('Preview'),
      '#open' => TRUE,
    ];

    // Get available preview providers from the resolver service.
    $preview_provider_options = [];
    foreach ($this->previewResolver->getProviders() as $provider_id => $provider) {
      $plugin_definition = $provider->getPluginDefinition();
      $preview_provider_options[$provider_id] = $plugin_definition['label'];
    }

    // Preview controls container (provider select and update button).
    $form['preview']['controls'] = [
      '#type' => 'container',
      '#attributes' => [
        'style' => 'display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem;',
      ],
    ];
    $form['preview']['controls']['preview_provider'] = [
      '#type' => 'select',
      '#title' => $this->t('Preview provider'),
      '#title_display' => 'invisible',
      '#options' => $preview_provider_options,
      '#default_value' => $form_state->getValue('preview_provider', 'json'),
      '#required' => TRUE,
      '#wrapper_attributes' => ['style' => 'margin: 0;'],
    ];
    $form['preview']['controls']['preview_refresh'] = [
      '#type' => 'submit',
      '#op' => 'refresh_preview',
      '#submit' => ['::multistepSubmit'],
      '#value' => $this->t('Update'),
      '#ajax' => [
        'callback' => '::buildPreviewAjax',
        'wrapper' => 'ce-display-preview-wrapper',
        'effect' => 'fade',
        'progress' => 'none',
      ],
      '#wrapper_attributes' => ['style' => 'margin: 0;'],
    ];

    // Preview content area (empty initially, populated on button click).
    $form['preview']['content'] = $this->buildPreview($form, $form_state);

    // In overviews involving nested rows from contributed modules (i.e.
    // field_group), the 'plugin type' selects can trigger a series of changes
    // in child rows. The #ajax behavior is therefore not attached directly to
    // the selects, but triggered by the client-side script through a hidden
    // #ajax 'Refresh' button. A hidden 'refresh_rows' input tracks the name of
    // affected rows.
    // This module doesn't need that, but is still using that functionality
    // (copied from the field_ui module), for the moment.
    $form['refresh_rows'] = ['#type' => 'hidden'];
    $form['refresh'] = [
      '#type' => 'submit',
      '#value' => $this->t('Refresh'),
      '#op' => 'refresh_table',
      '#submit' => ['::multistepSubmit'],
      '#ajax' => [
        'callback' => '::multistepAjax',
        'wrapper' => 'field-display-overview-wrapper',
        'effect' => 'fade',
        // The button stays hidden, so we hide the Ajax spinner too. Ad-hoc
        // spinners will be added manually by the client-side script.
        'progress' => 'none',
      ],
      '#attributes' => ['class' => ['visually-hidden']],
    ];

    $form['actions'] = ['#type' => 'actions'];
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#button_type' => 'primary',
      '#value' => $this->t('Save'),
    ];

    $form['#attached']['library'][] = 'custom_elements_ui/drupal.custom_elements_ui';

    return $form;
  }

  /**
   * Checks if Layout Builder option in our CE display is usable.
   *
   * This is split out into a separate private method, and returns several
   * values at once, to abstract away the convoluted logic of deriving them.
   *
   * @return array
   *   Four values indicating:
   *   - Whether the "Use layout builder" option should be visible (bool)
   *   - An extra message to add either to the "Use layout builder" option
   *     description, or if the option isn't visible: this is a general message
   *     to display on top of the screen.
   *   - The view mode to use in the link to the layout edit page. (Empty if
   *     the option is invisible, or if there is no logical single layout to
   *     refer to.)
   *   - Whether the other options should always remain visible (because they
   *     are used by view modes cannot use the "Use layout builder" option.) If
   *     false, these options have no functions when "Use layout builder" is
   *     enabled.
   */
  private function layoutBuilderEnabledOptions(): array {
    /** @var \Drupal\custom_elements\Entity\EntityCeDisplayInterface $ce_display */
    $ce_display = $this->getEntity();
    $id_bundle_prefix = $ce_display->getTargetEntityTypeId() . '.' . $ce_display->getTargetBundle() . '.';
    if ($ce_display->getMode() === 'default') {
      // Prepare: check existence of CE displays and entity view displays.
      $non_default_ce_display_ids = $this->entityTypeManager->getStorage('entity_ce_display')
        ->getQuery()
        ->accessCheck(FALSE)
        ->condition('id', $id_bundle_prefix, 'STARTS_WITH')
        ->condition('status', TRUE)
        ->condition('id', $id_bundle_prefix . 'default', '<>')
        ->execute();
      $ce_displays_in_keys = array_flip($non_default_ce_display_ids);

      $display_storage = $this->entityTypeManager->getStorage('entity_view_display');
      $view_display_ids = $display_storage->getQuery()
        ->accessCheck(FALSE)
        ->condition('id', $id_bundle_prefix, 'STARTS_WITH')
        ->condition('status', TRUE)
        ->condition('id', $id_bundle_prefix . 'default', '<>')
        ->execute();
      /** @var \Drupal\Core\Entity\Entity\EntityViewDisplay[] $non_default_view_displays */
      $non_default_view_displays = $display_storage->loadMultiple($view_display_ids);

      // Derive (view modes with) own displays which can/not use Layout Builder.
      $non_default_view_displays_not_using_lb = array_filter(
        $non_default_view_displays,
        fn($display) => !$display->getThirdPartySetting('layout_builder', 'enabled')
      );
      $non_default_view_displays_without_own_ce_not_using_lb = array_diff_key($non_default_view_displays_not_using_lb, $ce_displays_in_keys);

      // Get the default entity view display separately: it's also returned
      // if disabled/nonexistent in active configuration.
      $default_view_display = $this->entityDisplayRepository->getViewDisplay($ce_display->getTargetEntityTypeId(), $ce_display->getTargetBundle());

      // This default CE display can be used for building in several view modes.
      // Whether a Layout is used, is determined by this CE display's "use LB"
      // option, plus the "use LB" setting in:
      // - the default entity view display, for
      //   - "default" view mode,
      //   - any view mode that has no own entity view display AND no own CE
      //     display. This includes nonexistent view modes.
      // - their own entity view display, for any view mode where that exists
      //   AND which has no own CE display.
      if ($default_view_display->getThirdPartySetting('layout_builder', 'enabled')) {
        $enable_option = TRUE;
        $layout_link_view_mode = 'default';
        // This CE display's "use LB" option can influence building in all view
        // modes (including nonexistent ones), except:
        // 1. those with their own CE display; this speaks for itself, because
        //    they are visible in other tabs on the screen.
        // 2. those without their own CE display but with their own view display
        //    that does not have "use LB" enabled. Those are still using this
        //    CE display's other settings.
        // If the second category exists: add this info, because it is not
        // apparent.
        $extra_message = '';
        if ($non_default_view_displays_without_own_ce_not_using_lb) {
          $args = ['%modes' => $this->getViewModeLabelsFromDisplays($non_default_view_displays_without_own_ce_not_using_lb)];
          $extra_message = $non_default_ce_display_ids
            ? $this->t("This is done for all view modes that don't have their own custom element display, <em>except</em> %modes (which always use the below settings).", $args)
            : $this->t('This is done for all view modes <em>except</em> %modes (which always use the below settings).', $args);
        }
        $always_keep_other_settings_enabled = (bool) $non_default_view_displays_without_own_ce_not_using_lb;
      }
      else {
        // This CE display's "use LB" option can only influence building in view
        // modes without their own CE display but with their own view display
        // that has "use LB" enabled.
        $non_default_view_displays_using_lb = array_filter(
          $non_default_view_displays,
          fn($display) => $display->getThirdPartySetting('layout_builder', 'enabled')
        );
        $non_default_view_displays_without_own_ce_using_lb = array_diff_key($non_default_view_displays_using_lb, $ce_displays_in_keys);
        $enable_option = (bool) $non_default_view_displays_without_own_ce_using_lb;
        $layout_link_view_mode = count($non_default_view_displays_without_own_ce_using_lb) == 1
          ? current($non_default_view_displays_without_own_ce_using_lb)->getMode() : '';
        $extra_message = $enable_option ? $this->t('The default display has "use Layout Builder" disabled, so this option can only influence building custom elements for view mode: %lb_modes.', [
          '%lb_modes' => $this->getViewModeLabelsFromDisplays($non_default_view_displays_without_own_ce_using_lb),
        ]) : '';

        // This CE display's other settings are used by 'default' and all other
        // view modes without their own CE display (including nonexistent view
        // modes). If those view modes exist: extend the message for some extra
        // clarity. (Without naming them; that gets too long.)
        if ($enable_option && $non_default_view_displays_without_own_ce_not_using_lb) {
          $extra_message .= ' ' . ($non_default_ce_display_ids
            ? $this->t("Other view modes (that don't have their own custom element display) use the below settings.")
            : $this->t('Other view modes use the below settings.'));
        }
        $always_keep_other_settings_enabled = TRUE;
      }
    }
    else {
      // This non-default CE display's "use LB" option can influence building
      // (only in its own view mode), if:
      // - the corresponding entity view display exists and has use-LB enabled;
      // - it does not exist, but the default entity view display has use-LB
      //   enabled.
      // entityDisplayRepository::>getViewDisplay() cannot check if a display
      // actually exists, so do 'generic' loading.
      $displays = $this->entityTypeManager->getStorage('entity_view_display')
        ->loadMultiple([
          $id_bundle_prefix . $ce_display->getMode(),
          $id_bundle_prefix . 'default',
        ]);
      // Disabled displays are ignored. If none remain, LB is not enabled.
      $displays = array_filter(
        $displays,
        fn($display) => $display->status()
      );
      if (count($displays) > 1) {
        // Check the non-default display.
        unset($displays[$id_bundle_prefix . 'default']);
      }
      $enable_option = $displays && current($displays)->getThirdPartySetting('layout_builder', 'enabled');
      // If only the default display exists, the "edit" link should point to
      // it.
      $layout_link_view_mode = $enable_option ? current($displays)->getMode() : '';
      $extra_message = '';
      $always_keep_other_settings_enabled = FALSE;
    }
    return [$enable_option, $extra_message, $layout_link_view_mode, $always_keep_other_settings_enabled];
  }

  /**
   * Gets view modes from Entity (CE/view) displays, as comma separated string.
   */
  protected function getViewModeLabelsFromDisplays(array $displays): string {
    $view_mode_info = $this->entityDisplayRepository->getViewModes($this->entity->getTargetEntityTypeId());
    $labels = array_map(
      function ($display) use ($view_mode_info) {
        assert($display instanceof EntityDisplayInterface);
        return $view_mode_info[$display->getMode()]['label'] ?? $display->getMode();
      },
      $displays
    );
    return implode(', ', $labels);
  }

  /**
   * {@inheritdoc}
   */
  protected function getEntityDisplay($entity_type_id, $bundle, $mode) {
    return $this->getCustomElementGenerator()->getEntityCeDisplay($entity_type_id, $bundle, $mode);
  }

  /**
   * {@inheritdoc}
   */
  protected function getDefaultPlugin($field_type) {
    return 'auto';
  }

  /**
   * {@inheritdoc}
   */
  public function getRegions() {
    // This form only uses the "content" region for all fields.
    // Deletion is handled via Remove buttons, not regions.
    return [
      'content' => [
        'title' => $this->t('-'),
        'invisible' => TRUE,
        'message' => $this->t('No components are displayed.'),
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getRowRegion(&$row) {
    // All rows are in the 'content' region.
    // We use Remove button for deletion instead of region-based hiding.
    return 'content';
  }

  /**
   * Builds the table row structure for a single field.
   *
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   The field definition.
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   A table row array.
   *
   * @todo Redo this with UI revamp in #3455435. parent:form() currently loops
   *   through field definitions, but we likely want to loop through component
   *   names instead, so that multiple components for the same fields can be
   *   set. This means the parameter definition of buildFieldRow() likely isn't
   *   good to use anymore.
   * @todo At the same time (or later?), decide whether we can support other
   *   'kinds of formatters' (like static values, #3446287) in that same loop.
   *   If not, we'll need to create some separate loop later (just like
   *   parent::form() loops through the extraFields at the moment). Ordering
   *   likely doesn't matter much, since this is going to be reordered by
   *   weight later, anyway.
   */
  protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
    $display_options = NULL;
    $field_name = $field_definition->getName();
    $label = $field_definition->getLabel();
    $component_name = $this->getComponentNameFromFieldName($field_name, NULL, FALSE);
    if ($component_name) {
      $display_options = $this->entity->getComponent($component_name);

      // Disable fields without any applicable plugins.
      if (empty($this->getApplicablePluginOptions($field_definition))) {
        $this->entity->removeComponent($component_name);
        $display_options = $this->entity->getComponent($component_name);
      }
    }

    $field_row['human_name'] = [
      '#plain_text' => $label,
    ];
    $field_row['name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Attribute / Slot name'),
      '#title_display' => 'invisible',
      '#default_value' => $display_options['name'],
      '#size' => 20,
      '#required' => TRUE,
    ];
    $field_row['is_slot'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Is Slot'),
      '#title_display' => 'invisible',
      '#default_value' => $display_options['is_slot'] ?? FALSE,
    ];
    $field_row['formatter'] = [
      '#type' => 'select',
      '#title' => $this->t('Custom element formatter'),
      '#options' => $this->getApplicablePluginOptions($field_definition),
      '#title_display' => 'invisible',
      '#default_value' => $display_options['formatter'] ?? 'auto',
    ];
    if (!isset($field_row['formatter']['#options'][$field_row['formatter']['#default_value']])) {
      // Display warning, but do not prevent saving the value unmodified.
      $field_row['formatter']['#options'][$field_row['formatter']['#default_value']] = $this->t('Unknown: @value', ['@value' => $field_row['formatter']['#default_value']]);
      if (!$form_state->getUserInput()) {
        $this->messenger()->addWarning($this->t(
          "@label has unknown formatter %value.",
          ['@label' => $label, '%value' => $field_row['formatter']['#default_value']]
        ));
      }
    }
    $field_row += [
      '#attributes' => ['class' => ['draggable', 'tabledrag-leaf']],
      '#row_type' => 'field',
      '#region_callback' => [$this, 'getRowRegion'],
      '#js_settings' => [
        'rowHandler' => 'field',
        'defaultPlugin' => $this->getDefaultPlugin($field_definition->getType()),
      ],
      'weight' => [
        '#type' => 'textfield',
        '#title' => $this->t('Weight for @title', ['@title' => $label]),
        '#title_display' => 'invisible',
        '#default_value' => $display_options['weight'] ?? '0',
        '#size' => 3,
        '#attributes' => ['class' => ['field-weight']],
      ],
      'parent_wrapper' => [
        // Somehow FieldUiTable element needs this, so keep it.
        'parent' => [
          '#type' => 'hidden',
          '#empty_value' => '',
          '#attributes' => ['class' => ['js-field-parent', 'field-parent']],
          '#parents' => ['fields', $field_name, 'parent'],
        ],
        'hidden_name' => [
          '#type' => 'hidden',
          '#default_value' => $field_name,
          '#attributes' => ['class' => ['field-name']],
        ],
      ],
    ];

    // Get the corresponding plugin object.
    // @todo Safely remove the case for !$component_name in #3446485 / second
    //   call parameter, when all rows represent components.
    try {
      $plugin = $component_name ? $this->entity->getRenderer($component_name)
        : $this->entity->getRenderer($field_name, TRUE);
    }
    catch (PluginNotFoundException) {
      $plugin = NULL;
    }

    $field_row = $this->buildFieldRowPluginForm($field_row, $field_name, $plugin, $form, $form_state);

    // Actions column contains the Remove button.
    $field_row['actions'] = [
      '#type' => 'submit',
      '#value' => $this->t('Remove'),
      '#name' => 'remove_' . $field_name,
      '#submit' => ['::removeFieldSubmit'],
      '#ajax' => [
        'callback' => '::removeFieldAjax',
        'wrapper' => 'field-display-overview-wrapper',
      ],
      '#limit_validation_errors' => [],
    ];

    return $field_row;
  }

  /**
   * Builds the plugin form into the field row.
   *
   * @param array $field_row
   *   The field form row.
   * @param string $field_name
   *   The namae of the field.
   * @param \Drupal\custom_elements\CustomElementsFieldFormatterInterface|null $plugin
   *   The formatter plugin. Passing NULL will add enough items to the row, to
   *   have any further 'cells' in the row be indented correctly.
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   The updated field form row.
   */
  private function buildFieldRowPluginForm(array $field_row, string $field_name, ?CustomElementsFieldFormatterInterface $plugin, array $form, FormStateInterface $form_state) {
    // Base button element for the various plugin settings actions.
    $base_button = [
      '#submit' => ['::multistepSubmit'],
      '#ajax' => [
        'callback' => '::multistepAjax',
        'wrapper' => 'field-display-overview-wrapper',
        'effect' => 'fade',
      ],
      '#field_name' => $field_name,
    ];

    $field_row['plugin']['settings_edit_form']['form'] = [];
    if ($plugin) {
      $subform_state = SubformState::createForSubform($field_row['plugin']['settings_edit_form']['form'], $form, $form_state);
      $field_row['plugin']['settings_edit_form']['form'] = $plugin
        ->buildConfigurationForm($field_row['plugin']['settings_edit_form']['form'], $subform_state);
    }

    if ($plugin && $form_state->get('plugin_settings_edit') == $field_name) {
      // We are currently editing this field's plugin settings. Display the
      // settings form and submit buttons.
      if ($field_row['plugin']['settings_edit_form']['form']) {
        $field_row['plugin']['#cell_attributes'] = ['colspan' => 3];
        $field_row['plugin']['settings_edit_form'] = [
          '#type' => 'container',
          '#attributes' => ['class' => ['field-plugin-settings-edit-form']],
          '#parents' => ['fields', $field_name, 'settings_edit_form'],
          'label' => [
            '#markup' => $this->t('Plugin settings'),
          ],
          'form' => $field_row['plugin']['settings_edit_form']['form'],
          'actions' => [
            '#type' => 'actions',
            'save_settings' => $base_button + [
              '#type' => 'submit',
              '#validate' => ['::validatePluginConfigurationForm'],
              '#button_type' => 'primary',
              '#name' => $field_name . '_plugin_settings_update',
              '#value' => $this->t('Update'),
              '#op' => 'update',
            ],
            'cancel_settings' => $base_button + [
              '#type' => 'submit',
              '#name' => $field_name . '_plugin_settings_cancel',
              '#value' => $this->t('Cancel'),
              '#op' => 'cancel',
                // Do not check errors for the 'Cancel' button, but make sure we
                // get the value of the 'plugin type' select.
              '#limit_validation_errors' => [['fields', $field_name, 'type']],
            ],
          ],
        ];
        $field_row['#attributes']['class'][] = 'field-plugin-settings-editing';
      }
    }
    else {
      $field_row['settings_summary'] = [];
      $field_row['settings_edit'] = [];

      // Display a summary of the current plugin settings, and (if the
      // summary is not empty) a button to edit them.
      $summary = $plugin?->settingsSummary();

      if ($summary) {
        $field_row['settings_summary'] = [
          '#type' => 'inline_template',
          '#template' => '<div class="field-plugin-summary">{{ summary|safe_join("<br />") }}</div>',
          '#context' => ['summary' => $summary],
          '#cell_attributes' => ['class' => ['field-plugin-summary-cell']],
        ];
      }

      // When the plugin provides a form, show an edit button.
      // But actually hide the form for now.
      if (!empty($field_row['plugin']['settings_edit_form']['form'])) {
        unset($field_row['plugin']['settings_edit_form']['form']);
        $field_row['settings_edit'] = $base_button + [
          '#type' => 'image_button',
          '#name' => $field_name . '_settings_edit',
          '#src' => 'core/misc/icons/787878/cog.svg',
          '#attributes' => ['class' => ['field-plugin-settings-edit'], 'alt' => $this->t('Edit')],
          '#op' => 'edit',
            // Do not check errors for the 'Edit' button, but make sure we get
            // the value of the 'plugin formatter' select.
          '#limit_validation_errors' => [['fields', $field_name, 'formatter']],
          '#prefix' => '<div class="field-plugin-settings-edit-wrapper">',
          '#suffix' => '</div>',
        ];
      }
    }
    return $field_row;
  }

  /**
   * Gets component name from field name.
   *
   * This is temporary code as long as the UI still has one row per field.
   *
   * @return string
   *   The component name.
   *
   * @todo Remove this method; at the same time, change the component name for
   *   the 'add_select' case to still be unique.
   */
  private function getComponentNameFromFieldName(string $field_name, $entity = NULL, $log = TRUE): string {
    return $field_name;
  }

  /**
   * Gets the field definitions currently configured in the CE display.
   */
  private function getConfiguredFieldDefinitions(): array {
    $field_definitions = $this->getFieldDefinitions();
    $configured_field_definitions = [];
    foreach ($this->entity->getComponents() as $component) {
      $field_name = $component['field_name'];
      if (isset($field_definitions[$field_name])) {
        $configured_field_definitions[$field_name] = $field_definitions[$field_name];
      }
    }

    return $configured_field_definitions;
  }

  /**
   * Form validation callback for plugin configuration form.
   *
   * @param array $form
   *   A nested array of form elements comprising the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public function validatePluginConfigurationForm(array &$form, FormStateInterface $form_state) {
    // The entity was not populated yet on afterBuild(), because we cannot
    // guarantee correctness of all components when they are being renamed. So,
    // temporarily set current component into cloned entity to make sure we can
    // at least validate its configuration form.
    $validate_entity = clone $this->entity;
    $field_name = $form_state->getTriggeringElement()['#field_name'];
    $component_name = $this->getComponentNameFromFieldName($field_name, $validate_entity);

    // Allow the 'triggering' formatter to validate its configuration.
    $plugin = $validate_entity->getRenderer($component_name);
    if ($plugin) {
      $plugin_form =& $form['fields'][$field_name]['plugin']['settings_edit_form']['form'];
      $subform_state = SubformState::createForSubform($plugin_form, $form, $form_state);
      $plugin->validateConfigurationForm($plugin_form, $subform_state);
    }
    else {
      // Should never happen. Not a user-friendly message.
      $form_state->setError($form['fields'][$field_name]['name'], $this->t('Cannot find renderer for component @name', ['@name' => $component_name]));
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
    assert($entity instanceof EntityCeDisplayInterface);
    $form_values = $form_state->getValues();
    if ($this->entity instanceof EntityWithPluginCollectionInterface) {
      // Do not manually update values represented by plugin collections.
      $form_values = array_diff_key($form_values, $this->entity->getPluginCollections());
    }

    // Collect data for 'regular' fields.
    // @todo Change this structure in #3446485: $form keys will be component
    //   names, not field names. Rename '#fields' (here and elsewhere)?
    foreach ($form['#fields'] as $field_name) {
      $values = $form_values['fields'][$field_name];
      $component_name = $field_name;

      $options = $entity->getComponent($component_name);
      $options['formatter'] = $values['formatter'];
      $options['field_name'] = $field_name;
      $options['name'] = $values['name'];
      $options['is_slot'] = (bool) $values['is_slot'];
      $options['weight'] = $values['weight'];

      // Update field settings only if the submit handler told us to.
      if ($form_state->get('plugin_settings_update') === $field_name) {
        $component_name = $this->getComponentNameFromFieldName($field_name);
        // getRenderer() needs basic properties to be updated.
        $entity->setComponent($component_name, $options);
        $plugin = $entity->getRenderer($component_name);
        $plugin_form =& $form['fields'][$field_name]['plugin']['settings_edit_form']['form'];
        $subform_state = SubformState::createForSubform($plugin_form, $form, $form_state);
        $plugin->submitConfigurationForm($plugin_form, $subform_state);
        // Remove some fixed variables which are passed as part of
        // configuration from configuration storage.
        // @see \Drupal\custom_elements\Entity\EntityCeDisplay::getRenderer()
        $options['configuration'] = array_diff_key(
          $plugin->getConfiguration(),
          array_flip(['field_definition', 'view_mode', 'name', 'is_slot'])
        );
        $form_state->set('plugin_settings_update', NULL);
      }

      $entity->setComponent($component_name, $options);
    }
    // Sometimes during ajax form values miss the required form fields.
    if (!empty($form_values['custom_element_name'])) {
      $entity->setCustomElementName($form_values['custom_element_name']);
    }
    if (isset($form_values['force_auto'])) {
      $entity->setForceAutoProcessing($form_values['force_auto']);
    }
    if (isset($form_values['force_layout'])) {
      $entity->setUseLayoutBuilder($form_values['force_layout']);
    }
  }

  /**
   * Submit handler for removing a field.
   *
   * @param array $form
   *   Form elements.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   */
  public function removeFieldSubmit(array $form, FormStateInterface $form_state) {
    $trigger = $form_state->getTriggeringElement();

    // Extract field name from button name.
    if (isset($trigger['#name']) && strpos($trigger['#name'], 'remove_') === 0) {
      $field_name = substr($trigger['#name'], strlen('remove_'));

      // Get the component name for this field.
      $component_name = $this->getComponentNameFromFieldName($field_name, $this->entity, FALSE);

      if ($component_name) {
        // Remove the component from the entity immediately.
        $this->entity->removeComponent($component_name);
      }

      $form_state->setRebuild();
    }
  }

  /**
   * AJAX callback for removing a field.
   *
   * @param array $form
   *   Form elements.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   AJAX response to replace the field table and add field dropdown.
   */
  public function removeFieldAjax(array $form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();
    $response->addCommand(new ReplaceCommand('#field-display-overview-wrapper', $form['fields']));
    // Also update the "Add field" dropdown to show the removed field.
    $response->addCommand(new ReplaceCommand('.js-form-item-add-select', $form['add_select']));
    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function multistepAjax($form, FormStateInterface $form_state) {
    $trigger = $form_state->getTriggeringElement();
    if (isset($trigger['#name']) && $trigger['#name'] == 'add_select') {
      // Replace the whole table. (Don't call parent, which can only handle
      // its own known '#op's.)
      $response = new AjaxResponse();
      $response->addCommand(new ReplaceCommand('#field-display-overview-wrapper', $form['fields']));
      $response->addCommand(new ReplaceCommand('.js-form-item-add-select', $form['add_select']));
    }
    else {
      $response = parent::multistepAjax($form, $form_state);
    }
    $response->addCommand(new ReplaceCommand('#ce-display-preview-wrapper', $this->buildPreview($form, $form_state)));

    return $response;
  }

  /**
   * Ajax handler for preview.
   *
   * @param array $form
   *   Form elements.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Values associated with the form.
   *
   * @return Drupal\Core\Ajax\AjaxResponse
   *   Updated preview content.
   */
  public function buildPreviewAjax(array $form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();
    $response->addCommand(new ReplaceCommand('#ce-display-preview-wrapper', $this->buildPreview($form, $form_state)));
    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function save(array $form, FormStateInterface $form_state) {
    $components = $this->entity->getComponents();
    // Components are keyed by the form's 'fixed row value'. Key them by
    // component name as defined the schema.
    $new_components = [];
    foreach ($components as $form_row_name => $component) {
      $component_name = $component['name'];
      if (isset($new_components[$component_name])) {
        // Duplicate name; the first one is more likely to be the one we want.
        // Do not cancel; just don't save.
        // @todo figure out duplicate name detection after #3446485.
        $this->messenger()->addWarning(
          $this->t('Several components have the key %name. The component for row %row_name was not saved.',
            ['%name' => $component_name, '%row_name' => $form_row_name]
          ));
        continue;
      }

      // Remove temporary form-only properties before saving.
      unset($component['name']);
      if (isset($component['region'])) {
        unset($component['region']);
      }
      $new_components[$component_name] = $component;
    }

    // Key the changed components by their intended names; remove unused keys.
    $remove_components = array_fill_keys(array_keys($components), TRUE);
    foreach ($new_components as $component_name => $component) {
      $this->entity->setComponent($component_name, $component);
      unset($remove_components[$component_name]);
    }
    foreach (array_keys($remove_components) as $key) {
      $this->entity->removeComponent($key);
    }

    return parent::save($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  protected function getDisplayModes() {
    return $this->entityDisplayRepository->getViewModes($this->entity->getTargetEntityTypeId());
  }

  /**
   * {@inheritdoc}
   */
  protected function getDisplayModeOptions() {
    return $this->entityDisplayRepository->getViewModeOptions($this->entity->getTargetEntityTypeId());
  }

  /**
   * {@inheritdoc}
   */
  protected function getDisplayModesLink() {
    return [
      '#type' => 'link',
      '#title' => $this->t('Manage view modes'),
      '#url' => Url::fromRoute('entity.entity_view_mode.collection'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  protected function getOverviewUrl($mode) {
    $entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId());
    return Url::fromRoute('entity.entity_ce_display.' . $this->entity->getTargetEntityTypeId() . '.view_mode', [
      'view_mode_name' => $mode,
    ] + FieldUI::getRouteBundleParameter($entity_type, $this->entity->getTargetBundle()));
  }

  /**
   * Allows all fields and not only whose display is configurable.
   *
   * @return \Drupal\Core\Field\FieldDefinitionInterface[]
   *   The array of field definitions
   */
  protected function getFieldDefinitions() {
    return $this->entityFieldManager->getFieldDefinitions($this->entity->getTargetEntityTypeId(), $this->entity->getTargetBundle());
  }

  /**
   * {@inheritdoc}
   */
  protected function getApplicablePluginOptions(FieldDefinitionInterface $field_definition) {
    $options = $this->customElementFormatterPluginManager->getOptions($field_definition->getType());
    $applicable_options = [];
    foreach ($options as $plugin_id => $label) {
      $plugin_class = DefaultFactory::getPluginClass($plugin_id, $this->customElementFormatterPluginManager->getDefinition($plugin_id));
      try {
        if ($plugin_class::isApplicable($plugin_id, $field_definition)) {
          $applicable_options[$plugin_id] = $label;
        }
      }
      catch (\Exception $exception) {
        $this->logger('custom_elements')
          ->warning('Error when determining compatible plugins: @error', ['@error' => $exception->getMessage()]);
      }
    }
    return $applicable_options;
  }

  /**
   * Returns an array containing the table headers.
   *
   * @return array
   *   The table header.
   */
  protected function getTableHeader() {
    return [
      $this->t('Field'),
      $this->t('Key'),
      $this->t('Is Slot'),
      $this->t('Formatter'),
      $this->t('Weight'),
      // Hidden column for "parent_wrapper" element:
      '',
      ['data' => $this->t('Settings'), 'colspan' => 3],
      ['data' => $this->t('Action')],
    ];
  }

  /**
   * {@inheritdoc}
   */
  protected function thirdPartySettingsForm(PluginSettingsInterface $plugin, FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  protected function alterSettingsSummary(array &$summary, PluginSettingsInterface $plugin, FieldDefinitionInterface $field_definition) {}

  /**
   * Builds a preview of the custom elements display.
   *
   * @return array
   *   A render array containing the preview.
   */
  protected function buildPreview($form, FormStateInterface $form_state): array {
    $preview = [
      '#type' => 'container',
      '#attributes' => ['class' => ['custom-elements-preview']],
      '#prefix' => '<div id="ce-display-preview-wrapper">',
      '#suffix' => '</div>',
    ];

    // Check if preview should be generated (only when button is clicked).
    $triggering_element = $form_state->getTriggeringElement();
    $should_generate_preview = $triggering_element
      && isset($triggering_element['#op'])
      && $triggering_element['#op'] === 'refresh_preview';

    // Show empty area if preview hasn't been requested yet.
    if (!$should_generate_preview) {
      $preview['message'] = [
        '#markup' => '<p>' . $this->t('No preview. Select a preview provider and press <em>Update</em> to generate it.') . '</p>',
      ];
      return $preview;
    }

    /** @var \Drupal\custom_elements\Entity\EntityCeDisplayInterface $ce_display */
    $ce_display = clone $this->getEntity();
    // Update the cloned entity with current form values during AJAX callbacks.
    if ($form_state->isProcessingInput() && $triggering_element) {
      try {
        $this->copyFormValuesToEntity($ce_display, $form, $form_state);
      }
      catch (\Exception $e) {
        // If copying form values fails, use the current entity state.
        $this->logger('custom_elements')
          ->warning('Error copying values: @error', ['@error' => $e->getMessage()]);
      }
    }

    // Only generate preview if the display has components.
    $components = $ce_display->getComponents();
    if (empty($components)) {
      $preview['message'] = [
        '#markup' => '<p><em>' . $this->t('No fields configured for preview.') . '</em></p>',
      ];
      return $preview;
    }

    try {
      $entity_type_id = $ce_display->getTargetEntityTypeId();
      $bundle = $ce_display->getTargetBundle();
      $sample = $this->generateSampleEntity($entity_type_id, $bundle);

      // Get the selected preview provider.
      $preview_provider_id = $form_state->getValue(['preview', 'controls', 'preview_provider']);
      if (!$preview_provider_id) {
        $preview_provider_id = $form_state->getValue('preview_provider', 'json');
      }
      $provider = $this->previewResolver->getProviderById($preview_provider_id);
      if (!$provider) {
        // This should actually never happen.
        throw new \InvalidArgumentException(sprintf('Preview provider "%s" is not available as a configured service.', $preview_provider_id));
      }
      // Generate preview with the selected provider instance.
      $custom_element = $this->getCustomElementGenerator()
        ->generateWithCeDisplay($sample, $ce_display);
      $preview['content'] = $provider->preview($custom_element);
    }
    catch (\RuntimeException $e) {
      $preview['error'] = [
        '#markup' => '<p><em>' . $this->t('Preview generation failed: @message', ['@message' => $e->getMessage()]) . '</em></p>',
      ];
    }

    return $preview;
  }

  /**
   * Generates a sample entity for the given entity type and bundle.
   *
   * @param string $entity_type_id
   *   The entity type ID.
   * @param string $bundle
   *   The bundle name.
   *
   * @return \Drupal\Core\Entity\FieldableEntityInterface|null
   *   The sample entity or NULL if generation failed.
   */
  protected function generateSampleEntity(string $entity_type_id, string $bundle) {
    // Create entity with sample field values.
    try {
      $entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id);
      $storage = $this->entityTypeManager->getStorage($entity_type_id);

      $values = [];

      // Set bundle if entity type has bundles.
      if ($bundle_key = $entity_type_definition->getKey('bundle')) {
        $values[$bundle_key] = $bundle;
      }
      // Create entity with basic required values.
      $sample_entity = $storage->createWithSampleValues($bundle, $values);
      // Set a temporary ID to avoid "entity cannot have a URI" errors.
      $this->setIdOnSampleEntity($sample_entity);
      // Generate sample values for all fields that might miss the value.
      $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle);
      foreach ($field_definitions as $field_name => $field_definition) {
        // Skip read-only fields (like computed fields).
        if ($field_definition->isReadOnly()) {
          continue;
        }
        // Skip fields that are set already but make sure that id is set on
        // referenced entities.
        if (!$sample_entity->get($field_name)->isEmpty()) {
          $sample_value = $sample_entity->get($field_name)->getValue();
          foreach ($sample_value as $sample_value_item) {
            if (isset($sample_value_item['entity']) && $sample_value_item['entity'] instanceof EntityInterface) {
              $this->setIdOnSampleEntity($sample_value_item['entity']);
            }
          }
          continue;
        }
        try {
          $field_item_class = $field_definition->getItemDefinition()->getClass();
          if (method_exists($field_item_class, 'generateSampleValue')) {
            $sample_value = $field_item_class::generateSampleValue($field_definition);
            if (isset($sample_value['entity']) && $sample_value['entity'] instanceof EntityInterface) {
              $this->setIdOnSampleEntity($sample_value['entity']);
            }
            if (!empty($sample_value)) {
              $sample_entity->set($field_name, $sample_value);
            }
          }
        }
        catch (\Exception $e) {
          // Use default values if available, otherwise generate sample values.
          if ($default_value = $field_definition->getDefaultValueLiteral()) {
            $sample_entity->set($field_name, $default_value);
          }
          else {
            continue;
          }
        }
      }
      return $sample_entity;
    }
    catch (\Exception $e) {
      return NULL;
    }
  }

  /**
   * Set id on sample entity if it is not set.
   *
   * @param \Drupal\Core\Entity\EntityInterface $sample_entity
   *   Sample entity.
   *
   * @return void
   *   Sample entity with id set.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Random\RandomException
   */
  protected function setIdOnSampleEntity(EntityInterface $sample_entity) {
    $entity_type_definition = $this->entityTypeManager->getDefinition($sample_entity->getEntityTypeId());
    if ($entity_type_definition->hasKey('id')) {
      $id_key = $entity_type_definition->getKey('id');
      if ($sample_entity->get($id_key)->isEmpty()) {
        $sample_entity->set($id_key, random_int(999999, 9999999));
      }
    }
  }

}

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

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