countdown-8.x-1.8/src/Form/CountdownSettings.php

src/Form/CountdownSettings.php
<?php

declare(strict_types=1);

namespace Drupal\countdown\Form;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\countdown\CountdownConstants;
use Drupal\countdown\CountdownLibraryPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Admin settings form for the Countdown module.
 *
 * This form provides configuration for countdown libraries, loading methods,
 * visibility settings, and other module options. It features a live preview
 * that updates via AJAX as settings are changed.
 */
class CountdownSettings extends ConfigFormBase {

  /**
   * The theme handler service.
   *
   * @var \Drupal\Core\Extension\ThemeHandlerInterface
   */
  protected ThemeHandlerInterface $themeHandler;

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

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

  /**
   * The language manager service.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected LanguageManagerInterface $languageManager;

  /**
   * Constructs a CountdownSettings object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager
   *   The typed config manager.
   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
   *   The theme handler.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\countdown\CountdownLibraryPluginManager $plugin_manager
   *   The plugin manager.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager service.
   */
  public function __construct(
    ConfigFactoryInterface $config_factory,
    TypedConfigManagerInterface $typed_config_manager,
    ThemeHandlerInterface $theme_handler,
    ModuleHandlerInterface $module_handler,
    CountdownLibraryPluginManager $plugin_manager,
    LanguageManagerInterface $language_manager,
  ) {
    parent::__construct($config_factory, $typed_config_manager);
    $this->themeHandler = $theme_handler;
    $this->moduleHandler = $module_handler;
    $this->pluginManager = $plugin_manager;
    $this->languageManager = $language_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
      $container->get('config.typed'),
      $container->get('theme_handler'),
      $container->get('module_handler'),
      $container->get('plugin.manager.countdown_library'),
      $container->get('language_manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'countdown_admin_settings';
  }

  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {
    return ['countdown.settings'];
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildForm($form, $form_state);
    $config = $this->config('countdown.settings');

    // Method selection (Local vs CDN).
    $form['method'] = [
      '#type' => 'select',
      '#title' => $this->t('Loading Method'),
      '#options' => [
        'local' => $this->t('Local (Installed Libraries)'),
        'cdn' => $this->t('CDN (Remote Loading)'),
      ],
      '#default_value' => $form_state->getValue('method') ?: $config->get('method') ?: 'local',
      '#description' => $this->t('Choose how to load the countdown library. Local requires libraries to be installed in the libraries folder.'),
      '#ajax' => [
        'callback' => '::methodAjaxCallback',
        'wrapper' => 'library-area-wrapper',
        'event' => 'change',
        'effect' => 'fade',
        'progress' => ['type' => 'throbber', 'message' => $this->t('Loading libraries...')],
      ],
    ];

    // Library selection area.
    $form['library_area'] = [
      '#type' => 'container',
      '#attributes' => ['id' => 'library-area-wrapper'],
    ];

    // Library select.
    $current_method = $form_state->getValue('method') ?: $config->get('method') ?: 'local';
    $library_options = $this->buildLibraryOptions($current_method);

    $form['library_area']['library'] = [
      '#type' => 'select',
      '#title' => $this->t('Active Library'),
      '#options' => $library_options,
      '#empty_option' => $this->t('- None -'),
      '#empty_value' => 'none',
      '#default_value' => $form_state->getValue('library') ?: $config->get('library'),
      '#description' => $this->t('Select the countdown library to use.'),
      '#ajax' => [
        'callback' => '::libraryAjaxCallback',
        'wrapper' => 'library-ajax-wrapper',
        'event' => 'change',
        'progress' => ['type' => 'throbber', 'message' => $this->t('Loading library settings...')],
      ],
    ];

    // Library-dependent elements container.
    $form['library_area']['library_dependent'] = [
      '#type' => 'container',
      '#attributes' => ['id' => 'library-ajax-wrapper'],
    ];

    $this->buildLibraryDependentElements($form['library_area']['library_dependent'], $form_state);

    // Loading behavior section.
    $form['loading'] = [
      '#type' => 'details',
      '#title' => $this->t('Loading Behavior'),
      '#open' => TRUE,
    ];

    $form['loading']['load'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Automatically load library on pages'),
      '#default_value' => $config->get('load'),
      '#description' => $this->t('If enabled, the selected countdown library will be automatically loaded on pages based on the visibility settings below.'),
    ];

    // Build variant.
    $form['loading']['build'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Build Variant'),
      '#states' => [
        'visible' => [
          ':input[name="load"]' => ['checked' => TRUE],
        ],
      ],
    ];

    $form['loading']['build']['variant'] = [
      '#type' => 'radios',
      '#title' => $this->t('Library version'),
      '#options' => [
        0 => $this->t('Development (unminified, with source maps)'),
        1 => $this->t('Production (minified, optimized)'),
      ],
      '#default_value' => (int) $config->get('build.variant'),
      '#description' => $this->t('Production builds are smaller and load faster. Development builds are easier to debug.'),
      '#ajax' => [
        'callback' => '::previewAjaxCallback',
        'wrapper' => 'countdown-preview-wrapper',
      ],
    ];

    // Compute RTL availability once for conditional element building.
    $has_rtl = $this->hasAnyRtlLanguage();

    // RTL support.
    if ($has_rtl) {
      // Show the checkbox only when an RTL language exists.
      $form['loading']['rtl'] = [
        '#type' => 'checkbox',
        '#title' => $this->t('RTL Support'),
        '#default_value' => $config->get('rtl') ?? FALSE,
        '#description' => $this->t('Enable Right-to-Left language support for countdown displays.'),
        '#states' => [
          'visible' => [
            ':input[name="load"]' => ['checked' => TRUE],
          ],
        ],
        '#ajax' => [
          'callback' => '::previewAjaxCallback',
          'wrapper' => 'countdown-preview-wrapper',
        ],
      ];
    }
    else {
      // Hide the field entirely and force a FALSE value on submit.
      // A value element participates in form values without rendering.
      $form['loading']['rtl'] = [
        '#type' => 'value',
        '#value' => 0,
      ];
    }

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

    $form['advanced']['cache_lifetime'] = [
      '#type' => 'number',
      '#title' => $this->t('Cache Lifetime'),
      '#default_value' => $config->get('advanced.cache_lifetime') ?: 86400,
      '#description' => $this->t('How long to cache library discovery results in seconds. Default is 24 hours (86400 seconds).'),
      '#min' => 0,
      '#max' => 604800,
    ];

    $form['advanced']['debug_mode'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Debug Mode'),
      '#default_value' => $config->get('advanced.debug_mode') ?? FALSE,
      '#description' => $this->t('Enable detailed logging for troubleshooting library loading issues.'),
      '#ajax' => [
        'callback' => '::previewAjaxCallback',
        'wrapper' => 'countdown-preview-wrapper',
      ],
    ];

    // Visibility settings.
    $this->buildVisibilitySettings($form, $config);

    // Sidebar container for preview and meta.
    $form['countdown_sidebar'] = [
      '#type' => 'container',
      '#accordion' => TRUE,
    ];

    // Preview element in sidebar.
    $form['preview'] = [
      '#type' => 'container',
      '#group' => 'countdown_sidebar',
      '#weight' => -10,
    ];

    // Use the countdown_preview element.
    $form['preview']['content'] = [
      '#type' => 'countdown_preview',
      '#form_state' => $form_state,
      '#attributes' => ['id' => 'countdown-preview-wrapper'],
    ];

    // Meta element in sidebar.
    $form['meta'] = [
      '#type' => 'container',
      '#group' => 'countdown_sidebar',
    ];

    $form['meta']['content'] = [
      '#type' => 'countdown_meta',
      '#form_state' => $form_state,
      '#attributes' => ['id' => 'library-meta-wrapper'],
    ];

    // Hidden field to persist library status details open/closed state.
    $form['hide_status'] = [
      '#type' => 'hidden',
      '#default_value' => (bool) $config->get('hide'),
      '#attributes' => ['data-drupal-selector' => 'edit-hide-status'],
    ];

    $form['countdown_actions'] = [
      '#type' => 'container',
      '#weight' => -1,
      '#multilingual' => TRUE,
      '#attributes' => ['class' => ['countdown-sticky', 'gin-sticky', 'gin-sticky-form-actions']],
    ];

    // Move primary action buttons into a dedicated sidebar container.
    if (isset($form['actions'])) {
      // Move submit/cancel buttons into the action container.
      $form['countdown_actions']['actions'] = $form['actions'] ?? [];
      $form['countdown_actions']['actions']['#weight'] = 120;
      unset($form['actions']);
    }

    // Add visual toggler to open/close sidebar panel.
    $hide_panel = $this->t('Hide sidebar panel');
    $form['countdown_actions']['countdown_sidebar_toggle'] = [
      '#markup' => '
          <a href="#toggle-sidebar" class="meta-sidebar__trigger trigger" role="button" title="' . $hide_panel . '" aria-controls="countdown-sidebar">
            <span class="visually-hidden">' . $hide_panel . '</span>
          </a>',
      '#weight' => 999,
    ];

    // Add close button inside sidebar.
    $close_panel = $this->t('Close sidebar panel');
    $form['countdown_actions']['countdown_sidebar_close'] = [
      '#markup' => '
          <a href="#close-sidebar" class="meta-sidebar__close trigger" role="button" title="' . $close_panel . '">
            <span class="visually-hidden">' . $close_panel . '</span>
          </a>',
    ];

    // Add transparent overlay when sidebar is open.
    $form['countdown_sidebar_overlay'] = [
      '#markup' => '<div class="meta-sidebar__overlay trigger"></div>',
    ];

    // Attach admin library.
    $form['#attached']['library'][] = 'countdown/admin';

    // Add cache contexts.
    $form['#cache']['contexts'][] = 'user.permissions';
    $form['#cache']['tags'][] = CountdownConstants::CACHE_TAG_SETTINGS;

    // Final override: render this form with custom theme if defined.
    $form['#theme'] = 'countdown_form';

    return $form;
  }

  /**
   * Build library-dependent elements.
   *
   * @param array $element
   *   The element to build into.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  protected function buildLibraryDependentElements(array &$element, FormStateInterface $form_state): void {
    $config = $this->config('countdown.settings');
    $method = $form_state->getValue('method') ?: $config->get('method') ?: 'local';
    $library = $form_state->getValue('library') ?: $config->get('library');

    // CDN provider selection.
    if ($method === 'cdn' && $library && $library !== 'none') {
      // Build translatable option labels using a variable placeholder.
      $providers = [
        'jsdelivr' => 'jsDelivr',
        'unpkg' => 'unpkg',
      ];
      $provider_options = [];
      foreach ($providers as $id => $label) {
        // Use a placeholder to keep labels variable and translatable.
        $provider_options[$id] = $this->t('@label', ['@label' => $label]);
      }
      $element['cdn_provider'] = [
        '#type' => 'select',
        '#title' => $this->t('CDN Provider'),
        '#options' => $provider_options,
        '#default_value' => $form_state->getValue('cdn_provider') ?: $config->get('cdn.provider') ?: 'jsdelivr',
        '#ajax' => [
          'callback' => '::previewAjaxCallback',
          'wrapper' => 'countdown-preview-wrapper',
        ],
      ];
    }

    // Extensions for Tick library.
    if ($library === 'tick') {
      $element['extensions'] = [
        '#type' => 'details',
        '#title' => $this->t('Tick Extensions'),
        '#open' => TRUE,
      ];

      $plugin = $this->pluginManager->getPlugin('tick');
      if ($plugin && $plugin->hasExtensions()) {
        $extensions = $plugin->getAvailableExtensions();
        $saved = $config->get("file_assets.extensions.tick") ?: [];

        foreach ($extensions as $ext_id => $ext_info) {
          $element['extensions'][$ext_id] = [
            '#type' => 'checkbox',
            '#title' => $ext_info['label'],
            '#default_value' => in_array($ext_id, $saved),
            '#ajax' => [
              'callback' => '::previewAjaxCallback',
              'wrapper' => 'countdown-preview-wrapper',
            ],
          ];
        }
      }
    }
  }

  /**
   * Build visibility settings.
   *
   * @param array $form
   *   The form array.
   * @param object $config
   *   The configuration object.
   */
  protected function buildVisibilitySettings(array &$form, $config): void {
    $form['visibility'] = [
      '#type' => 'vertical_tabs',
      '#title' => $this->t('Visibility'),
    ];

    // Theme visibility.
    $form['theme_visibility'] = [
      '#type' => 'details',
      '#title' => $this->t('Themes'),
      '#open' => TRUE,
      '#group' => 'visibility',
    ];

    $themes = $this->themeHandler->listInfo();
    $theme_options = [];
    foreach ($themes as $theme_name => $theme) {
      if ($theme->status) {
        $theme_options[$theme_name] = $theme->info['name'];
      }
    }

    $form['theme_visibility']['themes'] = [
      '#type' => 'select',
      '#title' => $this->t('Themes'),
      '#options' => $theme_options,
      '#multiple' => TRUE,
      '#default_value' => $config->get('theme_groups.themes') ?: [],
      '#description' => $this->t('Select themes where the countdown library should be loaded.'),
    ];

    $form['theme_visibility']['theme_negate'] = [
      '#type' => 'radios',
      '#title' => $this->t('Load on'),
      '#options' => [
        0 => $this->t('All themes except those selected'),
        1 => $this->t('Only the selected themes'),
      ],
      '#default_value' => (int) $config->get('theme_groups.negate'),
    ];

    // Page visibility.
    $form['page_visibility'] = [
      '#type' => 'details',
      '#title' => $this->t('Pages'),
      '#group' => 'visibility',
    ];

    $pages = $config->get('request_path.pages') ?: [];
    if (is_array($pages)) {
      $pages = implode("\n", $pages);
    }

    $form['page_visibility']['pages'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Pages'),
      '#default_value' => $pages,
      '#description' => $this->t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. An example path is %admin-wildcard for every admin page. %front is the front page. You can also exclude pages by prefixing with '~'.", [
        '%admin-wildcard' => '/admin/*',
        '%front' => '<front>',
      ]),
    ];

    $form['page_visibility']['page_negate'] = [
      '#type' => 'radios',
      '#title' => $this->t('Load on'),
      '#options' => [
        0 => $this->t('All pages except those listed'),
        1 => $this->t('Only the listed pages'),
      ],
      '#default_value' => (int) $config->get('request_path.negate'),
    ];
  }

  /**
   * Build library options based on method.
   *
   * @param string $method
   *   The loading method ('local' or 'cdn').
   *
   * @return array
   *   Array of library options.
   */
  protected function buildLibraryOptions(string $method): array {
    if ($method === 'local') {
      return $this->pluginManager->getPluginOptions(TRUE, TRUE, FALSE);
    }

    $options = [];
    foreach ($this->pluginManager->getCdnCapablePlugins() as $plugin_id => $plugin) {
      $label = $plugin->getLabel();
      if ($version = $plugin->getRequiredVersion()) {
        $label .= ' (v' . $version . ')';
      }
      $options[$plugin_id] = $label;
    }
    return $options;
  }

  /**
   * AJAX callback for method changes.
   *
   * @param array $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The AJAX response.
   */
  public function methodAjaxCallback(array &$form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();

    // Replace the library area wrapper.
    $response->addCommand(new ReplaceCommand('#library-area-wrapper', $form['library_area']));

    // Update the meta and preview as well since library might have changed.
    $response->addCommand(new ReplaceCommand('#library-meta-wrapper', $form['meta']['content']));
    $response->addCommand(new ReplaceCommand('#countdown-preview-wrapper', $form['preview']['content']));

    return $response;
  }

  /**
   * AJAX callback for library changes.
   *
   * @param array $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The AJAX response.
   */
  public function libraryAjaxCallback(array &$form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();

    // Update library-dependent elements.
    $response->addCommand(new ReplaceCommand('#library-ajax-wrapper', $form['library_area']['library_dependent']));

    // Update meta to reflect new library info.
    $response->addCommand(new ReplaceCommand('#library-meta-wrapper', $form['meta']['content']));

    // Update preview with new library.
    $response->addCommand(new ReplaceCommand('#countdown-preview-wrapper', $form['preview']['content']));

    return $response;
  }

  /**
   * AJAX callback for preview updates.
   *
   * @param array $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The AJAX response.
   */
  public function previewAjaxCallback(array &$form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();

    // Only update the preview and meta.
    $response->addCommand(new ReplaceCommand('#library-meta-wrapper', $form['meta']['content']));
    $response->addCommand(new ReplaceCommand('#countdown-preview-wrapper', $form['preview']['content']));

    return $response;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValues();

    // Process pages into array.
    $pages_raw = $values['pages'] ?? '';
    $pages = $pages_raw !== '' ? _countdown_string_to_array($pages_raw) : [];

    // Process extensions if present.
    $library_extensions = [];
    $current_library = $values['library'];

    if ($current_library && $current_library !== 'none') {
      $plugin = $this->pluginManager->getPlugin($current_library);

      if ($plugin && $plugin->hasExtensions()) {
        // Get extensions values.
        $extensions_values = $values['extensions'] ?? [];
        $enabled = [];

        // Handle grouped extensions.
        foreach ($extensions_values as $key => $value) {
          if (is_array($value)) {
            // This is a group.
            foreach ($value as $ext_id => $enabled_flag) {
              if ($enabled_flag) {
                $enabled[] = $ext_id;
              }
            }
          }
          else {
            // This is a direct extension (not grouped).
            if ($value) {
              $enabled[] = $key;
            }
          }
        }

        $library_extensions[$current_library] = $enabled;
      }
    }

    // Enforce FALSE when no RTL language exists to prevent stale settings.
    $rtl = $this->hasAnyRtlLanguage() && $form_state->getValue('rtl');

    // Save configuration.
    $config = $this->config('countdown.settings');
    $config
      ->set('method', $values['method'])
      ->set('cdn.provider', $values['cdn_provider'] ?? 'jsdelivr')
      ->set('library', $values['library'])
      ->set('hide', $values['hide_status'])
      ->set('load', $values['load'])
      ->set('build.variant', (bool) $values['variant'])
      ->set('rtl', $rtl)
      ->set('theme_groups.negate', (int) $values['theme_negate'])
      ->set('theme_groups.themes', array_filter($values['themes'] ?? []))
      ->set('request_path.negate', (int) $values['page_negate'])
      ->set('request_path.pages', $pages)
      ->set('advanced.cache_lifetime', (int) ($values['cache_lifetime'] ?? 86400))
      ->set('advanced.debug_mode', (bool) ($values['debug_mode'] ?? FALSE));

    // Save extensions configuration.
    foreach ($library_extensions as $lib_id => $ext_list) {
      $config->set("file_assets.extensions.{$lib_id}", $ext_list);
    }

    $config->save();

    // Clear relevant caches.
    Cache::invalidateTags([
      CountdownConstants::CACHE_TAG_SETTINGS,
      CountdownConstants::CACHE_TAG_DISCOVERY,
      'library_info',
    ]);

    // Clear plugin manager cache.
    $this->pluginManager->resetAllCaches();

    parent::submitForm($form, $form_state);
  }

  /**
   * Checks if there is at least one RTL language configured on the site.
   *
   * Scans all languages (configured and available) and returns early on the
   * first RTL match. This is used to conditionally show the RTL checkbox in
   * the settings form.
   *
   * @return bool
   *   TRUE if any RTL language exists, FALSE otherwise.
   */
  protected function hasAnyRtlLanguage(): bool {
    // Request all known languages to catch configured and fallback entries.
    $languages = $this->languageManager->getLanguages(LanguageInterface::STATE_ALL);

    // Iterate once and return early on the first RTL language found.
    foreach ($languages as $language) {
      if ($language->getDirection() === LanguageInterface::DIRECTION_RTL) {
        return TRUE;
      }
    }
    return FALSE;
  }

}

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

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