countdown-8.x-1.8/modules/countdown_field/src/Plugin/Field/FieldFormatter/CountdownDefaultFormatter.php

modules/countdown_field/src/Plugin/Field/FieldFormatter/CountdownDefaultFormatter.php
<?php

declare(strict_types=1);

namespace Drupal\countdown_field\Plugin\Field\FieldFormatter;

use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\countdown\CountdownLibraryPluginManager;
use Drupal\countdown\Service\CountdownLibraryManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Plugin implementation of the 'countdown_default' formatter.
 *
 * This formatter displays countdown timers using the configured library. It
 * delegates to the plugin system for library attachment and JavaScript
 * settings preparation to ensure consistency across the module.
 *
 * @FieldFormatter(
 *   id = "countdown_default",
 *   label = @Translation("Countdown display"),
 *   field_types = {
 *     "countdown"
 *   }
 * )
 */
class CountdownDefaultFormatter extends FormatterBase 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 request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected RequestStack $requestStack;

  /**
   * The logger factory.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected LoggerChannelFactoryInterface $loggerFactory;

  /**
   * Constructs a CountdownDefaultFormatter object.
   *
   * @param string $plugin_id
   *   The plugin_id for the formatter.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   The definition of the field to which the formatter is associated.
   * @param array $settings
   *   The formatter settings.
   * @param string $label
   *   The formatter label display setting.
   * @param string $view_mode
   *   The view mode.
   * @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 \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger factory.
   */
  public function __construct(
    $plugin_id,
    $plugin_definition,
    FieldDefinitionInterface $field_definition,
    array $settings,
    $label,
    $view_mode,
    array $third_party_settings,
    CountdownLibraryManagerInterface $library_manager,
    CountdownLibraryPluginManager $plugin_manager,
    ConfigFactoryInterface $config_factory,
    RequestStack $request_stack,
    LoggerChannelFactoryInterface $logger_factory,
  ) {
    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
    $this->libraryManager = $library_manager;
    $this->pluginManager = $plugin_manager;
    $this->configFactory = $config_factory;
    $this->requestStack = $request_stack;
    $this->loggerFactory = $logger_factory;
  }

  /**
   * {@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['label'],
      $configuration['view_mode'],
      $configuration['third_party_settings'],
      $container->get('countdown.library_manager'),
      $container->get('plugin.manager.countdown_library'),
      $container->get('config.factory'),
      $container->get('request_stack'),
      $container->get('logger.factory'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public static function defaultSettings() {
    return [
      'show_event_name' => TRUE,
      'link_to_event' => TRUE,
      'wrapper_class' => '',
      'override_format' => FALSE,
      'format_override' => 'dhms',
      'show_finish_message' => TRUE,
      'finish_message_override' => '',
      'accessibility_label' => TRUE,
    ] + parent::defaultSettings();
  }

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

    $elements['show_event_name'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Show event name'),
      '#description' => $this->t('Display the event name if provided.'),
      '#default_value' => $this->getSetting('show_event_name'),
    ];

    $elements['link_to_event'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Link to event URL'),
      '#description' => $this->t('Make the event name a link if URL is provided.'),
      '#default_value' => $this->getSetting('link_to_event'),
      '#states' => [
        'visible' => [
          ':input[name="fields[' . $this->fieldDefinition->getName() . '][settings_edit_form][settings][show_event_name]"]' => ['checked' => TRUE],
        ],
      ],
    ];

    $elements['wrapper_class'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Wrapper CSS class'),
      '#description' => $this->t('Additional CSS class(es) for the countdown wrapper.'),
      '#default_value' => $this->getSetting('wrapper_class'),
      '#maxlength' => 255,
    ];

    $elements['override_format'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Override countdown format'),
      '#description' => $this->t('Override the display format set in the field value.'),
      '#default_value' => $this->getSetting('override_format'),
    ];

    $elements['format_override'] = [
      '#type' => 'select',
      '#title' => $this->t('Format'),
      '#options' => [
        'd' => $this->t('Days only'),
        'dh' => $this->t('Days and hours'),
        'dhm' => $this->t('Days, hours, and minutes'),
        'dhms' => $this->t('Days, hours, minutes, and seconds'),
        'hms' => $this->t('Hours, minutes, and seconds'),
        'ms' => $this->t('Minutes and seconds'),
        's' => $this->t('Seconds only'),
      ],
      '#default_value' => $this->getSetting('format_override'),
      '#states' => [
        'visible' => [
          ':input[name="fields[' . $this->fieldDefinition->getName() . '][settings_edit_form][settings][override_format]"]' => ['checked' => TRUE],
        ],
      ],
    ];

    $elements['show_finish_message'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Show finish message'),
      '#description' => $this->t('Display a message when the countdown completes.'),
      '#default_value' => $this->getSetting('show_finish_message'),
    ];

    $elements['finish_message_override'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Custom finish message'),
      '#description' => $this->t('Leave empty to use field value.'),
      '#default_value' => $this->getSetting('finish_message_override'),
      '#states' => [
        'visible' => [
          ':input[name="fields[' . $this->fieldDefinition->getName() . '][settings_edit_form][settings][show_finish_message]"]' => ['checked' => TRUE],
        ],
      ],
    ];

    $elements['accessibility_label'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Add accessibility label'),
      '#description' => $this->t('Add an aria-label for screen readers.'),
      '#default_value' => $this->getSetting('accessibility_label'),
    ];

    return $elements;
  }

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

    if ($this->getSetting('show_event_name')) {
      $summary[] = $this->t('Show event name');

      if ($this->getSetting('link_to_event')) {
        $summary[] = $this->t('Link to event URL');
      }
    }

    if ($wrapper_class = $this->getSetting('wrapper_class')) {
      $summary[] = $this->t('CSS class: @class', ['@class' => $wrapper_class]);
    }

    if ($this->getSetting('override_format')) {
      $formats = [
        'd' => $this->t('Days only'),
        'dh' => $this->t('Days and hours'),
        'dhm' => $this->t('Days, hours, minutes'),
        'dhms' => $this->t('Days, hours, minutes, seconds'),
        'hms' => $this->t('Hours, minutes, seconds'),
        'ms' => $this->t('Minutes and seconds'),
        's' => $this->t('Seconds only'),
      ];
      $format = $this->getSetting('format_override');
      if (isset($formats[$format])) {
        $summary[] = $this->t('Format: @format', ['@format' => $formats[$format]]);
      }
    }

    if ($this->getSetting('show_finish_message')) {
      $summary[] = $this->t('Show finish message');
      if ($override = $this->getSetting('finish_message_override')) {
        $summary[] = $this->t('Custom message: @message', ['@message' => $override]);
      }
    }

    if ($this->getSetting('accessibility_label')) {
      $summary[] = $this->t('Accessibility label enabled');
    }

    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = [];

    // Generate a unique field ID for this field instance.
    $field_id = $this->fieldDefinition->getName() . '-' . uniqid();

    // Check if the field is empty.
    $is_empty = TRUE;
    foreach ($items as $item) {
      if (!$item->isEmpty()) {
        $is_empty = FALSE;
        break;
      }
    }

    // Log debug information about the field state.
    if ($this->configFactory->get('countdown.settings')->get('debug.enabled')) {
      $logger = $this->loggerFactory->get('countdown_field');
      $logger->debug('viewElements called: field=@field, items=@count, empty=@empty', [
        '@field' => $this->fieldDefinition->getName(),
        '@count' => count($items),
        '@empty' => $is_empty ? 'yes' : 'no',
      ]);
    }

    // Process each field item.
    /** @var \Drupal\countdown_field\Plugin\Field\FieldType\CountdownItem $item */
    foreach ($items as $delta => $item) {
      // Get the library for this item, with fallback to default.
      $library = $this->getItemLibrary($item);

      // Get the plugin for the library.
      $plugin = $this->pluginManager->getPlugin($library);
      if (!$plugin) {
        // Log warning but continue to avoid breaking the page.
        $logger = $this->loggerFactory->get('countdown_field');
        $logger->warning('Library plugin not found: @library', [
          '@library' => $library,
        ]);
        continue;
      }

      // Get or generate target date.
      $target_date_iso = $this->getItemTargetDate($item);
      if (!$target_date_iso) {
        // If no valid date, create a placeholder element with libraries
        // attached.
        $elements[$delta] = $this->createPlaceholderElement($field_id, $delta, $item, $plugin);
        continue;
      }

      // Prepare countdown settings using the plugin's method.
      $settings = $this->prepareCountdownSettings($item, $plugin);

      // Determine the finish message.
      $finish_message = $this->getSetting('show_finish_message')
        ? ($this->getSetting('finish_message_override') ?: $item->finish_message)
        : '';

      // Build the render array using the field theme.
      $element = [
        '#theme' => 'countdown_field',
        '#library' => $library,
        '#target_date' => $target_date_iso,
        '#finish_message' => $finish_message,
        '#field_id' => $field_id . '-' . $delta,
        '#delta' => $delta,
        '#attributes' => [
          'class' => ['countdown', 'countdown-field'],
          'data-field-id' => $field_id . '-' . $delta,
          'data-library' => $library,
          'data-countdown-type' => 'field',
        ],
      ];

      // Add event details if enabled.
      if ($this->getSetting('show_event_name') && !empty($item->event_name)) {
        $element['#event_name'] = $item->event_name;

        if ($this->getSetting('link_to_event') && !empty($item->event_url)) {
          $element['#event_url'] = $item->event_url;
        }
      }

      // Add wrapper class if specified.
      if ($wrapper_class = $this->getSetting('wrapper_class')) {
        $element['#attributes']['class'][] = $wrapper_class;
      }

      // Add accessibility label if enabled.
      if ($this->getSetting('accessibility_label')) {
        $label = $this->t('Countdown timer for @name', ['@name' => $item->event_name ?: $this->t('event')]);
        $element['#attributes']['aria-label'] = $label;
      }

      // Attach the library using the plugin's buildAttachments method.
      $element = $this->attachLibrary($element, $item, $plugin);

      // Attach field-specific drupalSettings using unified namespace.
      $element['#attached']['drupalSettings']['countdown']['fields'][$field_id . '-' . $delta] = [
        'type' => 'field',
        'library' => $library,
        'target' => $target_date_iso,
        'settings' => $settings,
      ];

      // Build cache metadata.
      $element['#cache'] = [
        'contexts' => [
          'languages:language_interface',
          'timezone',
        ],
        'tags' => Cache::mergeTags(
          $this->fieldDefinition->getCacheTags(),
          ['countdown_library:' . $library]
        ),
        'max-age' => $this->calculateCacheMaxAge($item),
      ];

      $elements[$delta] = $element;
    }

    // If the field is completely empty, still attach basic libraries.
    if (empty($elements) && $is_empty) {
      $elements[0] = $this->createEmptyFieldElement($field_id);
    }

    return $elements;
  }

  /**
   * Get the library for an item with fallback to default.
   *
   * @param \Drupal\countdown_field\Plugin\Field\FieldType\CountdownItem $item
   *   The field item.
   *
   * @return string
   *   The library ID.
   */
  protected function getItemLibrary($item): string {
    // First check if item has a library set.
    if (!empty($item->library)) {
      return $item->library;
    }

    // Fall back to field storage default.
    $default = $this->fieldDefinition
      ->getFieldStorageDefinition()
      ->getSetting('default_library');

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

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

  /**
   * Get the target date for an item.
   *
   * @param \Drupal\countdown_field\Plugin\Field\FieldType\CountdownItem $item
   *   The field item.
   *
   * @return string|null
   *   The ISO-8601 date string or NULL.
   */
  protected function getItemTargetDate($item): ?string {
    // Check if item has a valid target date.
    if (!empty($item->target_date)) {
      return $item->getTargetDateIso();
    }

    // For new/empty items, generate a default future date.
    $default_date = new \DateTime('+7 days');
    return $default_date->format('Y-m-d\TH:i:s\Z');
  }

  /**
   * Create a placeholder element for items without dates.
   *
   * @param string $field_id
   *   The field identifier.
   * @param int $delta
   *   The field delta.
   * @param \Drupal\countdown_field\Plugin\Field\FieldType\CountdownItem $item
   *   The field item.
   * @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
   *   The library plugin.
   *
   * @return array
   *   The render array.
   */
  protected function createPlaceholderElement(string $field_id, int $delta, $item, $plugin): array {
    // Create a minimal element that still loads necessary libraries.
    $element = [
      '#type' => 'container',
      '#attributes' => [
        'class' => ['countdown-field', 'countdown-field-placeholder'],
        'data-field-id' => $field_id . '-' . $delta,
        'data-countdown-type' => 'field',
      ],
      '#children' => '',
    ];

    // Still attach libraries for consistency.
    $element = $this->attachLibrary($element, $item, $plugin);

    return $element;
  }

  /**
   * Create an element for completely empty fields.
   *
   * @param string $field_id
   *   The field identifier.
   *
   * @return array
   *   The render array.
   */
  protected function createEmptyFieldElement(string $field_id): array {
    // Create a minimal element that loads base libraries.
    $element = [
      '#type' => 'container',
      '#attributes' => [
        'class' => ['countdown-field', 'countdown-field-empty'],
        'data-field-id' => $field_id . '-0',
        'data-countdown-type' => 'field',
      ],
      '#children' => '',
    ];

    // Attach base field libraries.
    $element['#attached']['library'][] = 'countdown_field/integration';
    $element['#attached']['library'][] = 'countdown_field/formatter';

    return $element;
  }

  /**
   * Attach the countdown library using the plugin's buildAttachments method.
   *
   * This method leverages the main module's plugin system to properly attach
   * all required assets without duplicating logic.
   *
   * @param array $element
   *   The render element.
   * @param \Drupal\countdown_field\Plugin\Field\FieldType\CountdownItem $item
   *   The field item.
   * @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
   *   The library plugin.
   *
   * @return array
   *   The element with attached libraries.
   */
  protected function attachLibrary(array $element, $item, $plugin): array {
    // Get configuration for plugin attachment.
    $config = $this->configFactory->get('countdown.settings');

    // Get library settings from the field item.
    $library_settings = $item->getLibrarySettings();

    $attachment_config = [
      'method' => $item->getEffectiveMethod(),
      'variant' => $config->get('build.variant') ?? TRUE,
      'cdn_provider' => $config->get('cdn.provider') ?? 'jsdelivr',
      'rtl' => $config->get('appearance.rtl') ?? FALSE,
      'debug' => $config->get('debug.enabled') ?? FALSE,
      'context' => 'field',
      'field_config' => $library_settings,
    ];

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

    // Merge the attachments from the plugin.
    if (!empty($attachments['#attached'])) {
      $element['#attached'] = array_merge_recursive(
        $element['#attached'] ?? [],
        $attachments['#attached']
      );
    }

    // Always attach the field integration bridge.
    $element['#attached']['library'][] = 'countdown_field/integration';

    // Always attach the formatter library for CSS.
    $element['#attached']['library'][] = 'countdown_field/formatter';

    // Debug logging if enabled.
    if ($config->get('debug.enabled')) {
      $logger = $this->loggerFactory->get('countdown_field');
      $logger->debug('Libraries attached: @libs', [
        '@libs' => print_r($element['#attached']['library'] ?? [], TRUE),
      ]);
    }

    return $element;
  }

  /**
   * Prepare countdown settings for JavaScript using the plugin's method.
   *
   * This delegates JavaScript settings preparation to the plugin to ensure
   * consistency and avoid code duplication.
   *
   * @param \Drupal\countdown_field\Plugin\Field\FieldType\CountdownItem $item
   *   The countdown field item.
   * @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
   *   The library plugin.
   *
   * @return array
   *   The settings array for JavaScript initialization.
   */
  protected function prepareCountdownSettings($item, $plugin): array {
    // Get the stored library settings.
    $library_settings = $item->getLibrarySettings();
    $plugin_config = $library_settings[$item->library] ?? [];

    // Override finish message if formatter setting is enabled.
    if ($this->getSetting('show_finish_message')) {
      $override_message = $this->getSetting('finish_message_override');
      if ($override_message) {
        $plugin_config['finish_message'] = $override_message;
      }
      elseif (empty($plugin_config['finish_message'])) {
        // Ensure we always have a finish message.
        $plugin_config['finish_message'] = $item->finish_message ?: "Time's up!";
      }
    }

    // Let the plugin transform configuration to JavaScript settings.
    $settings = $plugin->getJavaScriptSettings($plugin_config);

    // Add library identifier.
    $settings['library'] = $item->library ?: $this->getItemLibrary($item);

    // Add format override if configured.
    if ($this->getSetting('override_format')) {
      $format_string = $this->getSetting('format_override');
      $settings['format'] = str_split($format_string);
    }

    return $settings;
  }

  /**
   * Calculate cache max age based on countdown target.
   *
   * For countdown timers, we cache until the target date is reached to ensure
   * the timer remains accurate. For count-up timers or past dates, we use a
   * standard cache duration.
   *
   * @param \Drupal\countdown_field\Plugin\Field\FieldType\CountdownItem $item
   *   The field item.
   *
   * @return int
   *   The cache max age in seconds.
   */
  protected function calculateCacheMaxAge($item): int {
    // For countdown timers, cache until the target date.
    if (!empty($item->target_date)) {
      $request = $this->requestStack->getCurrentRequest();
      $now = $request ? $request->server->get('REQUEST_TIME') : time();
      $diff = $item->target_date - $now;

      // Only cache if target is in the future.
      if ($diff > 0) {
        // Cache for at most 1 hour to allow for configuration changes.
        return min($diff, 3600);
      }
    }

    // Default to 1 hour cache for count-up or past dates.
    return 3600;
  }

}

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

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