countdown-8.x-1.8/modules/countdown_field/src/Plugin/Field/FieldWidget/CountdownWidget.php

modules/countdown_field/src/Plugin/Field/FieldWidget/CountdownWidget.php
<?php

declare(strict_types=1);

namespace Drupal\countdown_field\Plugin\Field\FieldWidget;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\countdown\CountdownLibraryPluginManager;
use Drupal\countdown\Service\CountdownLibraryManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Plugin implementation of the 'countdown_default' widget.
 *
 * This widget provides a datetime selector with library configuration for
 * countdown timers. It delegates library-specific configuration to the plugin
 * system to avoid code duplication.
 *
 * @FieldWidget(
 *   id = "countdown_default",
 *   label = @Translation("Countdown selector"),
 *   field_types = {
 *     "countdown"
 *   }
 * )
 */
class CountdownWidget extends WidgetBase implements ContainerFactoryPluginInterface {

  use StringTranslationTrait;

  /**
   * The countdown library manager.
   *
   * @var \Drupal\countdown\Service\CountdownLibraryManagerInterface
   */
  protected CountdownLibraryManagerInterface $libraryManager;

  /**
   * The countdown library plugin manager.
   *
   * @var \Drupal\countdown\CountdownLibraryPluginManager
   */
  protected CountdownLibraryPluginManager $pluginManager;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected ConfigFactoryInterface $configFactory;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected AccountProxyInterface $currentUser;

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

  /**
   * Constructs a CountdownWidget 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\countdown\Service\CountdownLibraryManagerInterface $library_manager
   *   The countdown library manager.
   * @param \Drupal\countdown\CountdownLibraryPluginManager $plugin_manager
   *   The countdown library plugin manager.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */
  public function __construct(
    $plugin_id,
    $plugin_definition,
    FieldDefinitionInterface $field_definition,
    array $settings,
    array $third_party_settings,
    CountdownLibraryManagerInterface $library_manager,
    CountdownLibraryPluginManager $plugin_manager,
    ConfigFactoryInterface $config_factory,
    AccountProxyInterface $current_user,
    EntityTypeManagerInterface $entity_type_manager,
  ) {
    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
    $this->libraryManager = $library_manager;
    $this->pluginManager = $plugin_manager;
    $this->configFactory = $config_factory;
    $this->currentUser = $current_user;
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $plugin_id,
      $plugin_definition,
      $configuration['field_definition'],
      $configuration['settings'],
      $configuration['third_party_settings'],
      $container->get('countdown.library_manager'),
      $container->get('plugin.manager.countdown_library'),
      $container->get('config.factory'),
      $container->get('current_user'),
      $container->get('entity_type.manager'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public static function defaultSettings() {
    return [
      'show_preview' => TRUE,
      'inline_labels' => FALSE,
      'timezone_handling' => 'site',
      'method_override' => FALSE,
    ] + parent::defaultSettings();
  }

  /**
   * {@inheritdoc}
   */
  public function settingsForm(array $form, FormStateInterface $form_state) {
    $elements = parent::settingsForm($form, $form_state);

    $elements['show_preview'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Show preview'),
      '#description' => $this->t('Display a live preview of the countdown timer.'),
      '#default_value' => $this->getSetting('show_preview'),
    ];

    $elements['inline_labels'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Inline labels'),
      '#description' => $this->t('Display form labels inline for a more compact layout.'),
      '#default_value' => $this->getSetting('inline_labels'),
    ];

    $elements['timezone_handling'] = [
      '#type' => 'select',
      '#title' => $this->t('Timezone handling'),
      '#options' => [
        'site' => $this->t('Site timezone'),
        'user' => $this->t('User timezone'),
        'utc' => $this->t('UTC'),
      ],
      '#default_value' => $this->getSetting('timezone_handling'),
      '#description' => $this->t('Timezone for date input and display.'),
    ];

    $elements['method_override'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Allow loading method override'),
      '#description' => $this->t('Allow users to override the global loading method (local/CDN).'),
      '#default_value' => $this->getSetting('method_override'),
    ];

    return $elements;
  }

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

    if ($this->getSetting('show_preview')) {
      $summary[] = $this->t('Preview enabled');
    }

    if ($this->getSetting('inline_labels')) {
      $summary[] = $this->t('Inline labels');
    }

    $timezone_options = [
      'site' => $this->t('Site timezone'),
      'user' => $this->t('User timezone'),
      'utc' => $this->t('UTC'),
    ];
    $timezone = $this->getSetting('timezone_handling');
    $summary[] = $this->t('Timezone: @timezone', ['@timezone' => $timezone_options[$timezone]]);

    if ($this->getSetting('method_override')) {
      $summary[] = $this->t('Method override allowed');
    }

    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    /** @var \Drupal\countdown_field\Plugin\Field\FieldType\CountdownItem $item */
    $item = $items[$delta];

    // Get the current loading method.
    $method = $this->libraryManager->getLoadingMethod();

    // Get allowed libraries for this field.
    $allowed_libraries = $item->getAllowedLibraries();
    $library_options = $this->getFilteredLibraryOptions($method, $allowed_libraries);

    // Check if we have any libraries available.
    if (empty($library_options)) {
      $element['no_libraries'] = [
        '#markup' => '<div class="messages messages--warning">' . $this->t('No countdown libraries are available. Please configure the Countdown module.') . '</div>',
      ];
      return $element;
    }

    // Generate unique wrapper ID for AJAX.
    $wrapper_id = 'countdown-widget-' . $delta . '-wrapper';

    // Build the main element container.
    $element['#type'] = 'details';
    $element['#open'] = TRUE;
    $element['#attributes'] = [
      'id' => $wrapper_id,
      'data-countdown-type' => 'field',
      'data-field-name' => $this->fieldDefinition->getName(),
    ];

    // Get the selected library with proper fallback to default.
    $selected_library = $this->getFormValue($form_state, $items, $delta, 'library');

    // Ensure we have a valid library selected for initial load.
    if (empty($selected_library) || !isset($library_options[$selected_library])) {
      // Try to get the default library from field storage settings.
      $default_library = $this->fieldDefinition
        ->getFieldStorageDefinition()
        ->getSetting('default_library');

      // If no field default, use global default.
      if (empty($default_library)) {
        $default_library = $this->libraryManager->getActiveLibrary();
      }

      // Validate the default library is available.
      if (isset($library_options[$default_library])) {
        $selected_library = $default_library;
      }
      else {
        // Fall back to first available library.
        $selected_library = key($library_options);
      }
    }

    // Library selection.
    $element['library'] = [
      '#type' => 'select',
      '#title' => $this->t('Countdown Library'),
      '#options' => $library_options,
      '#default_value' => $selected_library,
      '#required' => $this->fieldDefinition->isRequired(),
      '#ajax' => [
        'callback' => [$this, 'ajaxRebuildSettings'],
        'wrapper' => $wrapper_id,
        'progress' => [
          'type' => 'throbber',
          'message' => $this->t('Loading library settings...'),
        ],
      ],
    ];

    // Method override if allowed.
    if ($this->getSetting('method_override')) {
      $element['method'] = [
        '#type' => 'radios',
        '#title' => $this->t('Loading Method'),
        '#options' => [
          'local' => $this->t('Local'),
          'cdn' => $this->t('CDN'),
        ],
        '#default_value' => $item->method ?? $method,
      ];
    }

    // Library-specific settings wrapper.
    $element['library_settings_wrapper'] = [
      '#type' => 'container',
      '#tree' => TRUE,
    ];

    // Always build library settings if we have a selected library.
    if ($selected_library && isset($library_options[$selected_library])) {
      $this->buildLibrarySettings(
        $element['library_settings_wrapper'],
        $form_state,
        $items,
        $delta,
        $selected_library
      );
    }

    // Event details if allowed.
    if ($item->getFieldDefinition()->getSetting('allow_event_details')) {
      $element['event_details'] = [
        '#type' => 'details',
        '#title' => $this->t('Event Details'),
        '#open' => FALSE,
      ];

      $element['event_details']['event_name'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Event Name'),
        '#default_value' => $item->event_name ?? '',
        '#maxlength' => 255,
      ];

      $element['event_details']['event_url'] = [
        '#type' => 'url',
        '#title' => $this->t('Event URL'),
        '#default_value' => $item->event_url ?? '',
      ];
    }

    // Preview container if enabled.
    if ($this->getSetting('show_preview')) {
      $element['preview'] = [
        '#type' => 'container',
        '#attributes' => [
          'class' => ['countdown-widget-preview'],
          'id' => 'countdown-preview-' . $delta,
        ],
        '#weight' => 100,
      ];

      $element['preview']['label'] = [
        '#markup' => '<div class="countdown-widget-preview-label">' . $this->t('Preview') . '</div>',
      ];

      $element['preview']['content'] = [
        '#type' => 'container',
        '#attributes' => [
          'class' => ['countdown-preview-container'],
          'data-countdown-preview' => 'true',
        ],
      ];
    }

    // Attach widget library.
    $element['#attached']['library'][] = 'countdown_field/widget';

    // Attach preview settings using unified namespace.
    if ($this->getSetting('show_preview') && $selected_library) {
      $preview_id = 'preview-' . $delta;
      $element['#attached']['drupalSettings']['countdown']['previews'][$preview_id] = [
        'type' => 'preview',
        'library' => $selected_library,
        'target' => '+1 month',
        'settings' => [],
      ];
    }

    return $element;
  }

  /**
   * AJAX callback to rebuild the widget.
   *
   * @param array $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The updated form element.
   */
  public function ajaxRebuildSettings(array &$form, FormStateInterface $form_state) {
    $trigger = $form_state->getTriggeringElement();
    $parents = array_slice($trigger['#array_parents'], 0, -1);
    return NestedArray::getValue($form, $parents);
  }

  /**
   * Get the current form value during AJAX rebuilds.
   *
   * This method checks for user input during AJAX operations to maintain
   * state between library changes.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   The field items.
   * @param int $delta
   *   The field delta.
   * @param string $key
   *   The configuration key to retrieve.
   *
   * @return mixed
   *   The current form value or NULL.
   */
  protected function getFormValue(FormStateInterface $form_state, FieldItemListInterface $items, int $delta, string $key) {
    // Check user input for AJAX rebuilds.
    $user_input = $form_state->getUserInput();
    $field_name = $this->fieldDefinition->getName();

    // Map configuration keys to form structure.
    $input_map = [
      'library' => [$field_name, $delta, 'library'],
    ];

    if (isset($input_map[$key])) {
      $value = $user_input;
      foreach ($input_map[$key] as $part) {
        if (isset($value[$part])) {
          $value = $value[$part];
        }
        else {
          $value = NULL;
          break;
        }
      }

      if ($value !== NULL) {
        return $value;
      }
    }

    // Fall back to item value.
    if (isset($items[$delta])) {
      $item_value = $items[$delta]->$key ?? NULL;
      if ($item_value !== NULL) {
        return $item_value;
      }
    }

    // For library key specifically, add fallback to default library.
    if ($key === 'library') {
      // Try field storage default.
      $default = $this->fieldDefinition
        ->getFieldStorageDefinition()
        ->getSetting('default_library');

      if (!empty($default)) {
        return $default;
      }

      // Fall back to global default.
      return $this->libraryManager->getActiveLibrary();
    }

    return NULL;
  }

  /**
   * Build library-specific settings using the plugin's configuration form.
   *
   * This method delegates the entire configuration form building to the
   * plugin, which handles both common fields and library-specific settings.
   *
   * @param array &$element
   *   The form element to add settings to.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   The field items.
   * @param int $delta
   *   The field delta.
   * @param string $library
   *   The selected library ID.
   */
  protected function buildLibrarySettings(array &$element, FormStateInterface $form_state, FieldItemListInterface $items, int $delta, string $library): void {
    // Get the plugin instance from the plugin manager.
    $plugin = $this->pluginManager->getPlugin($library);

    if (!$plugin) {
      return;
    }

    // Get default values from configuration or user input.
    $user_input = $form_state->getUserInput();
    $field_name = $this->fieldDefinition->getName();
    $plugin_config = NULL;

    // Check for user input during AJAX rebuilds.
    if (isset($user_input[$field_name][$delta]['library_settings_wrapper'][$library])) {
      $plugin_config = $user_input[$field_name][$delta]['library_settings_wrapper'][$library];
    }
    elseif ($form_state->isRebuilding()) {
      // During rebuilds, check the form state values.
      $values = $form_state->getValues();
      if (isset($values[$field_name][$delta]['library_settings_wrapper'][$library])) {
        $plugin_config = $values[$field_name][$delta]['library_settings_wrapper'][$library];
      }
    }

    // Fall back to stored configuration.
    if ($plugin_config === NULL) {
      $item = $items[$delta] ?? NULL;
      if ($item) {
        $library_settings = $item->getLibrarySettings();
        $plugin_config = $library_settings[$library] ?? NULL;
      }

      // If still no config, use plugin defaults.
      if ($plugin_config === NULL) {
        $plugin_config = $plugin->getDefaultConfiguration();

        // If we have an existing item with a target date, use it.
        if ($item && !empty($item->target_date)) {
          // Ensure target_date is an integer before using gmdate.
          $timestamp = is_numeric($item->target_date) ? (int) $item->target_date : 0;
          if ($timestamp > 0) {
            // Convert timestamp to date string for plugin configuration.
            $plugin_config['target_date'] = gmdate('Y-m-d\TH:i:s', $timestamp);
          }
          $plugin_config['finish_message'] = $item->finish_message ?: "Time's up!";
        }
      }
    }

    // Initialize the array element before passing it to the plugin.
    $element[$library] = [];

    // Let the plugin build its entire configuration form.
    $plugin->buildConfigurationForm(
      $element[$library],
      $form_state,
      $plugin_config
    );

    // Add tree property to ensure proper form value structure.
    $element[$library]['#tree'] = TRUE;
  }

  /**
   * Get filtered library options based on method and allowed libraries.
   *
   * @param string $method
   *   The loading method ('local' or 'cdn').
   * @param array $allowed_libraries
   *   Array of allowed library IDs.
   *
   * @return array
   *   Filtered library options.
   */
  protected function getFilteredLibraryOptions(string $method, array $allowed_libraries): array {
    $all_options = $this->libraryManager->getAvailableLibraryOptions($method);

    if (empty($allowed_libraries)) {
      return $all_options;
    }

    // Filter to only allowed libraries.
    $filtered = [];
    foreach ($all_options as $id => $label) {
      if (in_array($id, $allowed_libraries, TRUE)) {
        $filtered[$id] = $label;
      }
    }

    return $filtered;
  }

  /**
   * Get the timezone for display based on widget settings.
   *
   * @return string|null
   *   The timezone identifier or NULL for default.
   */
  protected function getTimezoneForDisplay(): ?string {
    $handling = $this->getSetting('timezone_handling');

    switch ($handling) {
      case 'site':
        return $this->configFactory->get('system.date')->get('timezone.default');

      case 'user':
        if ($this->currentUser->isAuthenticated()) {
          $user_storage = $this->entityTypeManager->getStorage('user');
          $user = $user_storage->load($this->currentUser->id());
          if ($user) {
            $user_timezone = $user->getTimeZone();
            if ($user_timezone) {
              return $user_timezone;
            }
          }
        }
        // Fall back to site timezone if user has no preference.
        return $this->configFactory->get('system.date')->get('timezone.default');

      case 'utc':
        return 'UTC';

      default:
        return NULL;
    }
  }

  /**
   * Convert all DrupalDateTime objects to string format in configuration.
   *
   * This method recursively processes configuration arrays to ensure all
   * DrupalDateTime objects are converted to ISO-8601 strings for storage.
   *
   * @param mixed $data
   *   The data to process.
   *
   * @return mixed
   *   The processed data with DrupalDateTime objects converted to strings.
   */
  protected function convertDateTimeObjects($data) {
    if ($data instanceof DrupalDateTime) {
      // Convert DrupalDateTime to ISO-8601 string.
      return $data->format('Y-m-d\TH:i:s');
    }
    elseif ($data instanceof \DateTime) {
      // Handle regular DateTime objects too.
      return $data->format('Y-m-d\TH:i:s');
    }
    elseif (is_array($data)) {
      // Recursively process arrays.
      $processed = [];
      foreach ($data as $key => $value) {
        $processed[$key] = $this->convertDateTimeObjects($value);
      }
      return $processed;
    }
    else {
      // Return other data types as-is.
      return $data;
    }
  }

  /**
   * Extract datetime value from various possible formats.
   *
   * The datetime form element can return values in different formats depending
   * on the context. This method normalizes them to a timestamp.
   *
   * @param mixed $date_value
   *   The date value in various possible formats.
   *
   * @return int|null
   *   The UTC timestamp or NULL if invalid.
   */
  protected function extractDateTimeValue($date_value): ?int {
    // Handle NULL or empty values.
    if (empty($date_value)) {
      return NULL;
    }

    // Handle DrupalDateTime object.
    if ($date_value instanceof DrupalDateTime) {
      try {
        // Get the timezone used for input.
        $input_timezone = $this->getTimezoneForDisplay();

        // If the date doesn't have a timezone, set it.
        if ($input_timezone) {
          $date_value->setTimezone(new \DateTimeZone($input_timezone));
        }

        // Convert to UTC for storage.
        $utc_date = clone $date_value;
        $utc_date->setTimezone(new \DateTimeZone('UTC'));

        return (int) $utc_date->getTimestamp();
      }
      catch (\Exception $e) {
        return NULL;
      }
    }

    // Handle regular DateTime object.
    if ($date_value instanceof \DateTime) {
      try {
        $utc_date = clone $date_value;
        $utc_date->setTimezone(new \DateTimeZone('UTC'));
        return (int) $utc_date->getTimestamp();
      }
      catch (\Exception $e) {
        return NULL;
      }
    }

    // Handle array format from datetime form element.
    if (is_array($date_value)) {
      // Check for object key containing DrupalDateTime.
      if (isset($date_value['object']) && $date_value['object'] instanceof DrupalDateTime) {
        return $this->extractDateTimeValue($date_value['object']);
      }

      // Check for date and time keys.
      if (isset($date_value['date']) && isset($date_value['time'])) {
        try {
          // Combine date and time into ISO format.
          $datetime_string = $date_value['date'] . 'T' . $date_value['time'];
          $datetime = new \DateTime($datetime_string);
          return (int) $datetime->getTimestamp();
        }
        catch (\Exception $e) {
          return NULL;
        }
      }

      // Check for value key (some form elements use this).
      if (isset($date_value['value'])) {
        return $this->extractDateTimeValue($date_value['value']);
      }

      // No recognizable array format.
      return NULL;
    }

    // Handle numeric timestamp.
    if (is_numeric($date_value)) {
      $timestamp = (int) $date_value;
      // Validate reasonable timestamp range (year 1970 to 2100).
      if ($timestamp > 0 && $timestamp < 4102444800) {
        return $timestamp;
      }
      return NULL;
    }

    // Handle string date format.
    if (is_string($date_value)) {
      try {
        $datetime = new \DateTime($date_value);
        return (int) $datetime->getTimestamp();
      }
      catch (\Exception $e) {
        return NULL;
      }
    }

    // Unknown format.
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
    foreach ($values as &$value) {
      // Get the selected library.
      $library = $value['library'] ?? '';

      // Get plugin values from the wrapper.
      if ($library && isset($value['library_settings_wrapper'][$library])) {
        $plugin_values = $value['library_settings_wrapper'][$library];

        // Extract and set target_date at the root level.
        if (isset($plugin_values['target_date'])) {
          $timestamp = $this->extractDateTimeValue($plugin_values['target_date']);
          if ($timestamp !== NULL && $timestamp > 0) {
            $datetime = DrupalDateTime::createFromTimestamp($timestamp);
            $value['target_date'] = $datetime;
            // Also keep it in plugin_values as a string for library_settings.
            $plugin_values['target_date'] = gmdate('Y-m-d\TH:i:s', $timestamp);
          }
          else {
            // If invalid or empty, ensure it's unset.
            $value['target_date'] = NULL;
            $plugin_values['target_date'] = '';
          }
        }
        else {
          // No target_date in plugin values, ensure it's unset.
          $value['target_date'] = NULL;
        }

        // Map common plugin fields to field values.
        $value['finish_message'] = $plugin_values['finish_message'] ?? "Time's up!";

        // Convert all remaining DrupalDateTime objects.
        $processed_plugin_values = $this->convertDateTimeObjects($plugin_values);

        // Flatten library_specific values.
        $flattened_config = [];
        foreach ($processed_plugin_values as $key => $val) {
          if ($key === 'library_specific' && is_array($val)) {
            // Merge library_specific values into the flat structure.
            foreach ($val as $nested_key => $nested_val) {
              $flattened_config[$nested_key] = $nested_val;
            }
          }
          else {
            $flattened_config[$key] = $val;
          }
        }

        // Store the flattened configuration.
        $value['library_settings'] = [$library => $flattened_config];

        // Clean up the wrapper.
        unset($value['library_settings_wrapper']);
      }
      else {
        // No library selected or no plugin values, ensure target_date is NULL.
        $value['target_date'] = NULL;
      }

      // Handle event details.
      if (isset($value['event_details'])) {
        $value['event_name'] = $value['event_details']['event_name'] ?? '';
        $value['event_url'] = $value['event_details']['event_url'] ?? '';
        unset($value['event_details']);
      }
    }

    return $values;
  }

  /**
   * {@inheritdoc}
   */
  public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
    parent::extractFormValues($items, $form, $form_state);

    // Validate plugin configuration after extraction.
    $field_name = $this->fieldDefinition->getName();
    $values = $form_state->getValues();

    foreach ($items as $delta => $item) {
      if (!empty($item->library) && isset($values[$field_name][$delta]['library_settings_wrapper'][$item->library])) {
        $plugin = $this->pluginManager->getPlugin($item->library);

        if ($plugin) {
          // Create a new FormState for plugin validation.
          $plugin_form_state = new FormState();
          $plugin_values = $values[$field_name][$delta]['library_settings_wrapper'][$item->library];
          $plugin_form_state->setValues($plugin_values);

          // Build a dummy form element for validation.
          $plugin_form = [];
          $plugin->buildConfigurationForm($plugin_form, $plugin_form_state, $plugin_values);

          // Let the plugin validate its configuration.
          $plugin->validateConfigurationForm($plugin_form, $plugin_form_state);

          // Copy any errors back to the main form state with adjusted paths.
          foreach ($plugin_form_state->getErrors() as $name => $error) {
            // Adjust the error path to include the field wrapper.
            $adjusted_name = $field_name . '][' . $delta . '][library_settings_wrapper][' . $item->library . '][' . $name;
            $form_state->setErrorByName($adjusted_name, $error);
          }
        }
      }
    }
  }

}

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

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