countdown-8.x-1.8/modules/countdown_block/src/Plugin/Block/CountdownBlock.php

modules/countdown_block/src/Plugin/Block/CountdownBlock.php
<?php

declare(strict_types=1);

namespace Drupal\countdown_block\Plugin\Block;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\countdown\CountdownLibraryPluginManager;
use Drupal\countdown\Service\CountdownLibraryDiscoveryInterface;
use Drupal\countdown\Service\CountdownLibraryManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a configurable countdown timer block.
 *
 * This block allows users to add countdown timers to any region with
 * per-block configuration for library, loading method, and timer settings.
 * It delegates library-specific configuration to the plugin system.
 *
 * @Block(
 *   id = "countdown_block",
 *   admin_label = @Translation("Countdown Timer"),
 *   category = @Translation("Countdown")
 * )
 */
class CountdownBlock extends BlockBase implements ContainerFactoryPluginInterface {

  use StringTranslationTrait;

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

  /**
   * The countdown library discovery service.
   *
   * @var \Drupal\countdown\Service\CountdownLibraryDiscoveryInterface
   */
  protected CountdownLibraryDiscoveryInterface $libraryDiscovery;

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

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

  /**
   * Constructs a new CountdownBlock instance.
   *
   * @param array $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin ID.
   * @param mixed $plugin_definition
   *   The plugin definition.
   * @param \Drupal\countdown\Service\CountdownLibraryManagerInterface $library_manager
   *   The countdown library manager service.
   * @param \Drupal\countdown\Service\CountdownLibraryDiscoveryInterface $library_discovery
   *   The countdown library discovery service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The configuration factory.
   * @param \Drupal\countdown\CountdownLibraryPluginManager $plugin_manager
   *   The countdown library plugin manager.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    CountdownLibraryManagerInterface $library_manager,
    CountdownLibraryDiscoveryInterface $library_discovery,
    ConfigFactoryInterface $config_factory,
    CountdownLibraryPluginManager $plugin_manager,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->libraryManager = $library_manager;
    $this->libraryDiscovery = $library_discovery;
    $this->configFactory = $config_factory;
    $this->pluginManager = $plugin_manager;
  }

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

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    // Provide sensible defaults for new blocks.
    return [
      'method' => 'local',
      'library' => 'countdown',
      'cdn_provider' => 'jsdelivr',
      'event_name' => '',
      'event_url' => '',
      'plugin_config' => [],
    ];
  }

  /**
   * Get configuration value with fallback to global settings.
   *
   * This is used for form defaults and runtime configuration resolution.
   *
   * @param string $key
   *   The configuration key.
   * @param mixed $default
   *   The default value if not found.
   *
   * @return mixed
   *   The configuration value.
   */
  protected function getConfigWithFallback(string $key, $default = NULL) {
    // Check block configuration first.
    if (!empty($this->configuration[$key])) {
      return $this->configuration[$key];
    }

    // Fall back to global settings.
    $config = $this->configFactory->get('countdown.settings');
    $map = [
      'method' => 'method',
      'library' => 'library',
      'cdn_provider' => 'cdn.provider',
    ];

    if (isset($map[$key])) {
      $value = $config->get($map[$key]);
      if ($value !== NULL && $value !== '') {
        return $value;
      }
    }

    // Return the provided default or a sensible default.
    if ($default !== NULL) {
      return $default;
    }

    // Provide sensible defaults for critical settings.
    switch ($key) {
      case 'method':
        return 'local';

      case 'library':
        return 'countdown';

      case 'cdn_provider':
        return 'jsdelivr';

      default:
        return NULL;
    }
  }

  /**
   * Get the current form value during AJAX rebuilds.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param string $key
   *   The configuration key.
   * @param mixed $default
   *   The default value if not found.
   *
   * @return mixed
   *   The current form value or default.
   */
  protected function getFormValue(FormStateInterface $form_state, string $key, $default = NULL) {
    // Check user input for AJAX rebuilds.
    $user_input = $form_state->getUserInput();

    // Map configuration keys to form structure.
    $input_map = [
      'method' => ['settings', 'method'],
      'library' => ['settings', 'library_wrapper', 'library'],
      'cdn_provider' => ['settings', 'library_wrapper', 'cdn_provider'],
    ];

    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 configuration.
    return $this->configuration[$key] ?? $default;
  }

  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) {
    // Get the current method, checking user input for AJAX rebuilds.
    $method = $this->getFormValue($form_state, 'method')
      ?: $this->getConfigWithFallback('method', 'local');

    // Loading method selection.
    $form['method'] = [
      '#type' => 'radios',
      '#title' => $this->t('Loading Method'),
      '#description' => $this->t('Choose how to load countdown libraries.'),
      '#options' => [
        'local' => $this->t('Local (installed libraries only)'),
        'cdn' => $this->t('CDN (external libraries)'),
      ],
      '#default_value' => $method,
      '#required' => TRUE,
      '#ajax' => [
        'callback' => [$this, 'updateLibraryOptions'],
        'wrapper' => 'countdown-library-wrapper',
        'event' => 'change',
      ],
    ];

    // Library selection container with AJAX wrapper.
    $form['library_wrapper'] = [
      '#type' => 'container',
      '#attributes' => ['id' => 'countdown-library-wrapper'],
    ];

    // Get available libraries based on the selected method.
    $library_options = $this->getLibraryOptions($method);

    // Get current library, checking user input for AJAX rebuilds.
    $current_library = $this->getFormValue($form_state, 'library')
      ?: $this->getConfigWithFallback('library', 'countdown');

    // Validate the library is available for the current method.
    if (!isset($library_options[$current_library])) {
      $current_library = key($library_options) ?: '';
    }

    $form['library_wrapper']['library'] = [
      '#type' => 'select',
      '#title' => $this->t('Countdown Library'),
      '#description' => $this->t('Select the countdown library to use.'),
      '#options' => $library_options,
      '#default_value' => $current_library,
      '#required' => TRUE,
      '#empty_option' => $this->t('- Select a library -'),
      '#ajax' => [
        'callback' => [$this, 'updateLibrarySettings'],
        'wrapper' => 'countdown-plugin-settings-wrapper',
        'event' => 'change',
      ],
    ];

    // CDN provider selection (only visible for CDN method).
    if ($method === 'cdn') {
      $cdn_provider = $this->getFormValue($form_state, 'cdn_provider')
        ?: $this->getConfigWithFallback('cdn_provider', 'jsdelivr');

      $form['library_wrapper']['cdn_provider'] = [
        '#type' => 'select',
        '#title' => $this->t('CDN Provider'),
        '#description' => $this->t('Select the CDN provider to use.'),
        '#options' => [
          'jsdelivr' => $this->t('jsDelivr'),
          'cdnjs' => $this->t('cdnjs'),
          'unpkg' => $this->t('unpkg'),
        ],
        '#default_value' => $cdn_provider,
        '#required' => TRUE,
      ];
    }

    // Countdown event settings.
    $form['event'] = [
      '#type' => 'details',
      '#title' => $this->t('Event'),
      '#description' => $this->t('Configure countdown event settings.'),
      '#open' => TRUE,
      '#weight' => 90,
    ];

    // Block-specific settings that don't belong to the plugin.
    $form['event']['event_name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Event Name'),
      '#description' => $this->t('Optional name for the countdown event.'),
      '#default_value' => $this->configuration['event_name'],
      '#maxlength' => 255,
    ];

    $form['event']['event_url'] = [
      '#type' => 'url',
      '#title' => $this->t('Event URL'),
      '#description' => $this->t('Optional link to more information about the event.'),
      '#default_value' => $this->configuration['event_url'],
    ];

    // Plugin configuration container.
    $form['plugin_settings_wrapper'] = [
      '#type' => 'container',
      '#attributes' => ['id' => 'countdown-plugin-settings-wrapper'],
    ];

    // Add plugin-specific configuration if a library is selected.
    if (!empty($current_library)) {
      $this->buildPluginSettings($form['plugin_settings_wrapper'], $form_state, $current_library);
    }

    return $form;
  }

  /**
   * AJAX callback to update library options when method changes.
   *
   * @param array $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The updated form element.
   */
  public function updateLibraryOptions(array &$form, FormStateInterface $form_state) {
    return $form['settings']['library_wrapper'];
  }

  /**
   * AJAX callback to update library-specific settings.
   *
   * @param array $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The updated form element.
   */
  public function updateLibrarySettings(array &$form, FormStateInterface $form_state) {
    return $form['settings']['plugin_settings_wrapper'];
  }

  /**
   * Get available library options based on loading method.
   *
   * @param string $method
   *   The loading method ('local' or 'cdn').
   *
   * @return array
   *   Array of library options keyed by library ID.
   */
  protected function getLibraryOptions(string $method): array {
    return $this->libraryManager->getAvailableLibraryOptions($method);
  }

  /**
   * Build plugin-specific settings form elements.
   *
   * Delegates the entire configuration form 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 string $library
   *   The selected library ID.
   */
  protected function buildPluginSettings(array &$element, FormStateInterface $form_state, string $library) {
    // 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();
    $plugin_config = NULL;

    // Check for user input during AJAX rebuilds.
    if (isset($user_input['settings']['plugin_settings_wrapper'][$library])) {
      $plugin_config = $user_input['settings']['plugin_settings_wrapper'][$library];
    }

    // Fall back to stored configuration.
    if ($plugin_config === NULL) {
      $plugin_config = $this->configuration[$library]
                       ?? $plugin->getDefaultConfiguration();
    }

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

    // Let the plugin build its entire configuration form. This includes
    // common fields and library-specific fields.
    $plugin->buildConfigurationForm(
      $element[$library],
      $form_state,
      $plugin_config
    );
  }

  /**
   * {@inheritdoc}
   */
  public function blockValidate($form, FormStateInterface $form_state) {
    // Get the current values before any modification.
    $values = $form_state->getValues();
    $library = $values['library_wrapper']['library'] ?? NULL;

    if ($library) {
      $plugin = $this->pluginManager->getPlugin($library);

      if ($plugin && isset($form['plugin_settings_wrapper'][$library])) {
        // Get the plugin values.
        $plugin_values = $values['plugin_settings_wrapper'][$library] ?? [];

        // Create a completely new FormState for plugin validation to avoid
        // corrupting the main form_state.
        $plugin_form_state = new FormState();
        $plugin_form_state->setValues($plugin_values);

        // Let the plugin validate its configuration.
        $plugin_form_element = $form['plugin_settings_wrapper'][$library];
        $plugin->validateConfigurationForm($plugin_form_element, $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 plugin wrapper.
          $adjusted_name = 'plugin_settings_wrapper][' . $library . '][' . $name;
          $form_state->setErrorByName($adjusted_name, $error);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    // Get the complete form values BEFORE calling parent.
    $complete_values = $form_state->getValues();

    // Use our saved complete values for processing.
    $this->configuration['method'] = $complete_values['method'] ?? 'local';
    $this->configuration['library'] = $complete_values['library_wrapper']['library'] ?? '';

    if ($this->configuration['method'] === 'cdn') {
      $this->configuration['cdn_provider'] = $complete_values['library_wrapper']['cdn_provider'] ?? 'jsdelivr';
    }

    // Save block-specific settings.
    $this->configuration['event_name'] = $complete_values['event']['event_name'] ?? '';
    $this->configuration['event_url'] = $complete_values['event']['event_url'] ?? '';

    // Save plugin configuration with proper flattening of library_specific
    // values so they can be retrieved correctly.
    $library = $this->configuration['library'];
    if ($library && isset($complete_values['plugin_settings_wrapper'][$library])) {
      $plugin_values = $complete_values['plugin_settings_wrapper'][$library];

      // Convert DrupalDateTime objects to strings before flattening.
      array_walk_recursive($plugin_values, function (&$value) {
        if ($value instanceof DrupalDateTime) {
          $value = $value->format('Y-m-d\TH:i:s');
        }
      });

      // Flatten the configuration properly.
      $flattened_config = [];

      // First copy all top-level values (common fields).
      foreach ($plugin_values as $key => $value) {
        if ($key !== 'library_specific') {
          // Convert DrupalDateTime to string for storage.
          if ($value instanceof DrupalDateTime) {
            $flattened_config[$key] = $value->format('Y-m-d\TH:i:s');
          }
          else {
            $flattened_config[$key] = $value;
          }
        }
      }

      // Then merge library_specific values to top level.
      if (isset($plugin_values['library_specific']) && is_array($plugin_values['library_specific'])) {
        // Recursively flatten nested arrays in library_specific.
        $this->flattenArray($plugin_values['library_specific'], $flattened_config);
      }

      // Save the flattened configuration.
      $this->configuration[$library] = $flattened_config;
    }
  }

  /**
   * Flatten a nested array into a single-level array.
   *
   * This helper method flattens nested configuration arrays so that all
   * values are accessible at the top level for the plugin's getConfigValue()
   * method.
   *
   * @param array $array
   *   The array to flatten.
   * @param array &$result
   *   The result array to merge values into.
   * @param string $prefix
   *   Optional prefix for nested keys (not used for library_specific).
   */
  protected function flattenArray(array $array, array &$result, string $prefix = ''): void {
    foreach ($array as $key => $value) {
      // Skip certain nested structures that should remain nested.
      $preserve_nested = [
        'headings',
        'labels',
        'theme_options',
        'stop_configuration',
        'dots_settings',
        'line_settings',
        'boom_settings',
        'swap_settings',
        'advanced',
        'animation',
        'display',
        'appearance',
        'performance',
        'developer',
        'transforms',
        'transitions',
      ];

      if (in_array($key, $preserve_nested, TRUE) && is_array($value)) {
        // For these special nested configs, keep them as arrays but still
        // extract individual values to top level for backward compatibility.
        $result[$key] = $value;

        // Also flatten the values to top level for direct access.
        foreach ($value as $nested_key => $nested_value) {
          if (!is_array($nested_value)) {
            $result[$nested_key] = $nested_value;
          }
        }
      }
      elseif (is_array($value) && !in_array($key, ['format', 'show_units', 'labels', 'headings'], TRUE)) {
        // For other arrays, continue flattening recursively unless they're
        // known array values like format checkboxes.
        $this->flattenArray($value, $result);
      }
      else {
        // Direct assignment for non-array values or preserved arrays.
        $result[$key] = $value;
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    // Get block configuration with fallbacks for unconfigured blocks.
    $library = $this->getConfigWithFallback('library', 'countdown');
    $method = $this->getConfigWithFallback('method', 'local');

    // Get the plugin instance.
    $plugin = $this->pluginManager->getPlugin($library);

    if (!$plugin) {
      return [
        '#markup' => $this->t('Library @library not found.', ['@library' => $library]),
      ];
    }

    // Get plugin configuration, using defaults if not configured.
    $plugin_config = $this->configuration[$library] ?? $plugin->getDefaultConfiguration();

    // Ensure we have a target date. If not, provide a default future date
    // (1 day from now) to avoid errors on first placement.
    if (empty($plugin_config['target_date'])) {
      $default_date = new \DateTime('+1 day');
      $plugin_config['target_date'] = $default_date->format('Y-m-d\TH:i:s');
    }

    // Prepare countdown settings for JavaScript.
    $settings = $this->prepareCountdownSettings($plugin, $plugin_config);

    // Generate a unique block ID.
    $block_id = 'countdown-block-' . ($this->getDerivativeId() ?: uniqid());

    // Build the render array using the main module's theme.
    $build = [
      '#theme' => 'countdown',
      '#library' => $library,
      '#target_date' => $plugin_config['target_date'],
      '#countdown_event_name' => $this->configuration['event_name'],
      '#countdown_url' => $this->configuration['event_url'],
      '#attributes' => [
        'id' => $block_id,
        'class' => ['countdown-block'],
        'data-block-id' => $block_id,
        'data-countdown-library' => $library,
        'data-countdown-target' => $plugin_config['target_date'],
        'data-countdown-settings' => Json::encode($settings),
        'data-countdown-type' => 'block',
      ],
      '#cache' => [
        'contexts' => [
          'languages:language_interface',
          'timezone',
          'route',
        ],
        'tags' => $this->getCacheTags(),
        'max-age' => $this->getCacheMaxAge(),
      ],
    ];

    // Get configuration for plugin attachment.
    $config = $this->configFactory->get('countdown.settings');
    $attachment_config = [
      'method' => $method,
      'variant' => $config->get('build.variant'),
      'cdn_provider' => $this->getConfigWithFallback('cdn_provider', 'jsdelivr'),
      'rtl' => $config->get('appearance.rtl'),
      'debug' => $config->get('debug.enabled'),
      'context' => 'block',
      'block_config' => $this->configuration,
    ];

    // Let the plugin build its attachments with context-aware configuration.
    $plugin_attachments = $plugin->buildAttachments($attachment_config);

    // Merge plugin attachments with our build.
    if (!empty($plugin_attachments['#attached'])) {
      $build['#attached'] = $plugin_attachments['#attached'];
    }

    // Attach the bridge integration for countdown_block.
    $build['#attached']['library'][] = 'countdown_block/integration';

    // Attach block-specific configuration to drupalSettings.
    $build['#attached']['drupalSettings']['countdown']['blocks'][$block_id] = [
      'type' => 'block',
      'library' => $library,
      'target' => $plugin_config['target_date'],
      'settings' => $settings,
    ];

    return $build;
  }

  /**
   * Prepare countdown settings for JavaScript.
   *
   * Delegates to the plugin to transform configuration into JavaScript
   * settings, then adds block-specific settings.
   *
   * @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
   *   The countdown library plugin.
   * @param array $plugin_config
   *   The plugin configuration.
   *
   * @return array
   *   The settings array for JavaScript initialization.
   */
  protected function prepareCountdownSettings($plugin, array $plugin_config): array {
    // Get JavaScript settings from the plugin.
    $settings = $plugin->getJavaScriptSettings($plugin_config);

    // Add block-level settings.
    $settings['library'] = $this->getConfigWithFallback('library', 'countdown');
    $settings['method'] = $this->getConfigWithFallback('method', 'local');

    // Add CDN provider if using CDN method.
    if ($this->getConfigWithFallback('method', 'local') === 'cdn') {
      $settings['cdnProvider'] = $this->getConfigWithFallback('cdn_provider', 'jsdelivr');
    }

    // Add event information if provided.
    if (!empty($this->configuration['event_name'])) {
      $settings['eventName'] = $this->configuration['event_name'];
    }

    if (!empty($this->configuration['event_url'])) {
      $settings['eventUrl'] = $this->configuration['event_url'];
    }

    return $settings;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    $library = $this->getConfigWithFallback('library', 'countdown');

    return Cache::mergeTags(parent::getCacheTags(), [
      'config:countdown.settings',
      'countdown_library:' . $library,
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    // Get plugin configuration to check target date.
    $library = $this->getConfigWithFallback('library', 'countdown');
    $plugin_config = $this->configuration[$library] ?? [];

    // Calculate cache max age based on target date.
    if (!empty($plugin_config['target_date'])) {
      try {
        $target = new \DateTime($plugin_config['target_date']);
        $now = new \DateTime();
        $diff = $target->getTimestamp() - $now->getTimestamp();

        // Cache until the countdown expires, but not longer than 1 hour.
        if ($diff > 0) {
          return min($diff, 3600);
        }
      }
      catch (\Exception $e) {
        // If date parsing fails, use default cache time.
      }
    }

    // Default to 1 hour cache.
    return 3600;
  }

}

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

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