countdown-8.x-1.8/src/Plugin/CountdownLibrary/Tick.php

src/Plugin/CountdownLibrary/Tick.php
<?php

declare(strict_types=1);

namespace Drupal\countdown\Plugin\CountdownLibrary;

use Drupal\Core\Form\FormStateInterface;
use Drupal\countdown\Plugin\CountdownLibraryPluginBase;

/**
 * PQINA Tick Counter library plugin implementation.
 *
 * Tick is a highly customizable counter with multiple views, themes,
 * and extensive configuration options. It supports various display
 * modes and animations through modular extensions.
 *
 * @CountdownLibrary(
 *   id = "tick",
 *   label = @Translation("PQINA Tick Counter"),
 *   description = @Translation("Highly customizable counter with multiple views, themes, and animation styles"),
 *   type = "external",
 *   homepage = "https://pqina.nl/tick",
 *   repository = "https://github.com/pqina/tick",
 *   version = "1.8.3",
 *   npm_package = "@pqina/tick",
 *   folder_names = {
 *     "tick",
 *     "@pqina-tick",
 *     "pqina-tick",
 *     "tick-master",
 *     "pqina-tick-master"
 *   },
 *   files = {
 *     "css" = {
 *       "development" = "dist/core/tick.core.css",
 *       "production" = "dist/core/tick.core.min.css"
 *     },
 *     "js" = {
 *       "development" = "dist/core/tick.core.global.js",
 *       "production" = "dist/core/tick.core.global.min.js"
 *     }
 *   },
 *   required_files = {
 *     "dist/core/tick.core.global.js",
 *     "dist/core/tick.core.css"
 *   },
 *   alternative_paths = {
 *     {
 *       "dist/tick.js",
 *       "dist/tick.css"
 *     },
 *     {
 *       "lib/tick.js",
 *       "lib/tick.css"
 *     },
 *     {
 *       "build/tick.core.js",
 *       "build/tick.core.css"
 *     }
 *   },
 *   init_function = "Tick",
 *   author = "PQINA",
 *   license = "MIT",
 *   dependencies = {},
 *   cdn = {
 *     "jsdelivr" = {
 *       "css" = "//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/core/tick.core.min.css",
 *       "js" = "//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/core/tick.core.global.min.js"
 *     },
 *     "unpkg" = {
 *       "css" = "//unpkg.com/@pqina/tick@1.8.3/dist/core/tick.core.min.css",
 *       "js" = "//unpkg.com/@pqina/tick@1.8.3/dist/core/tick.core.global.min.js"
 *     },
 *     "cdnjs" = {
 *       "css" = "//cdnjs.cloudflare.com/ajax/libs/tick/1.8.3/tick.core.min.css",
 *       "js" = "//cdnjs.cloudflare.com/ajax/libs/tick/1.8.3/tick.core.global.min.js"
 *     }
 *   },
 *   weight = 3,
 *   experimental = false,
 *   api_version = "1.0"
 * )
 */
final class Tick extends CountdownLibraryPluginBase {

  /**
   * Available Tick extensions with their configurations.
   *
   * @var array
   */
  protected const EXTENSIONS = [
    'font_highres' => [
      'label' => 'High Resolution Font',
      'description' => 'High-quality font rendering for displays',
      'files' => [
        'development' => 'dist/font-highres/tick.font.highres.global.js',
        'production' => 'dist/font-highres/tick.font.highres.global.min.js',
      ],
      'cdn' => [
        'jsdelivr' => '//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/font-highres/tick.font.highres.global.min.js',
        'unpkg' => '//unpkg.com/@pqina/tick@1.8.3/dist/font-highres/tick.font.highres.global.min.js',
      ],
    ],
    'font_lowres' => [
      'label' => 'Low Resolution Font',
      'description' => 'Optimized font for smaller displays',
      'files' => [
        'development' => 'dist/font-lowres/tick.font.lowres.global.js',
        'production' => 'dist/font-lowres/tick.font.lowres.global.min.js',
      ],
      'cdn' => [
        'jsdelivr' => '//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/font-lowres/tick.font.lowres.global.min.js',
        'unpkg' => '//unpkg.com/@pqina/tick@1.8.3/dist/font-lowres/tick.font.lowres.global.min.js',
      ],
    ],
    'view_boom' => [
      'label' => 'Boom View',
      'description' => 'Explosive animation effect with sound',
      'css' => [
        'development' => 'dist/view-boom/tick.view.boom.css',
        'production' => 'dist/view-boom/tick.view.boom.min.css',
      ],
      'js' => [
        'development' => 'dist/view-boom/tick.view.boom.global.js',
        'production' => 'dist/view-boom/tick.view.boom.global.min.js',
      ],
      'cdn' => [
        'jsdelivr' => [
          'css' => '//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/view-boom/tick.view.boom.min.css',
          'js' => '//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/view-boom/tick.view.boom.global.min.js',
        ],
      ],
    ],
    'view_dots' => [
      'label' => 'Dots View',
      'description' => 'Dot matrix display style',
      'css' => [
        'development' => 'dist/view-dots/tick.view.dots.css',
        'production' => 'dist/view-dots/tick.view.dots.min.css',
      ],
      'js' => [
        'development' => 'dist/view-dots/tick.view.dots.global.js',
        'production' => 'dist/view-dots/tick.view.dots.global.min.js',
      ],
      'cdn' => [
        'jsdelivr' => [
          'css' => '//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/view-dots/tick.view.dots.min.css',
          'js' => '//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/view-dots/tick.view.dots.global.min.js',
        ],
      ],
    ],
    'view_line' => [
      'label' => 'Line View',
      'description' => 'Linear progress indicator',
      'css' => [
        'development' => 'dist/view-line/tick.view.line.css',
        'production' => 'dist/view-line/tick.view.line.min.css',
      ],
      'js' => [
        'development' => 'dist/view-line/tick.view.line.global.js',
        'production' => 'dist/view-line/tick.view.line.global.min.js',
      ],
      'cdn' => [
        'jsdelivr' => [
          'css' => '//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/view-line/tick.view.line.min.css',
          'js' => '//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/view-line/tick.view.line.global.min.js',
        ],
      ],
    ],
    'view_swap' => [
      'label' => 'Swap View',
      'description' => 'Smooth swapping animation',
      'css' => [
        'development' => 'dist/view-swap/tick.view.swap.css',
        'production' => 'dist/view-swap/tick.view.swap.min.css',
      ],
      'js' => [
        'development' => 'dist/view-swap/tick.view.swap.global.js',
        'production' => 'dist/view-swap/tick.view.swap.global.min.js',
      ],
      'cdn' => [
        'jsdelivr' => [
          'css' => '//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/view-swap/tick.view.swap.min.css',
          'js' => '//cdn.jsdelivr.net/npm/@pqina/tick@1.8.3/dist/view-swap/tick.view.swap.global.min.js',
        ],
      ],
    ],
  ];

  /**
   * Available transforms for Tick library.
   *
   * @var array
   */
  protected const TRANSFORMS = [
    'pad' => 'Pad values with zeros or custom character',
    'round' => 'Round numeric values',
    'ceil' => 'Round up to nearest integer',
    'floor' => 'Round down to nearest integer',
    'fraction' => 'Convert to fraction between min and max',
    'percentage' => 'Convert to percentage',
    'multiply' => 'Multiply by value',
    'divide' => 'Divide by value',
    'add' => 'Add value',
    'subtract' => 'Subtract value',
    'modulus' => 'Modulo operation',
    'abs' => 'Absolute value',
    'limit' => 'Limit between min and max',
    'upper' => 'Convert to uppercase',
    'lower' => 'Convert to lowercase',
    'split' => 'Split string by character',
    'replace' => 'Replace string patterns',
    'format' => 'Format with template',
    'plural' => 'Pluralize text',
    'chars' => 'Convert to character codes',
    'ascii' => 'Convert to ASCII codes',
    'tween' => 'Smooth transition animation',
    'spring' => 'Spring physics animation',
    'step' => 'Step-based animation',
    'arrive' => 'Arrival animation',
    'delay' => 'Delay animation',
  ];

  /**
   * Available transitions for Tick library.
   *
   * @var array
   */
  protected const TRANSITIONS = [
    'crossfade' => 'Fade in/out transition',
    'swap' => 'Swap animation with direction',
    'revolve' => 'Revolve around axis',
    'zoom' => 'Zoom in/out effect',
    'fade' => 'Simple fade effect',
    'move' => 'Move along axis',
    'rotate' => 'Rotate around axis',
    'scale' => 'Scale transformation',
  ];

  /**
   * {@inheritdoc}
   */
  protected function detectVersionCustom(string $base_path): ?string {
    // Check the main JS file for version information.
    $js_files = [
      '/dist/core/tick.core.global.js',
      '/dist/core/tick.core.global.min.js',
      '/dist/tick.js',
      '/lib/tick.js',
    ];

    // Try multiple strategies to detect Tick version.
    foreach ($js_files as $js_file) {
      $file_path = $base_path . $js_file;

      if (file_exists($file_path)) {
        try {
          // Read first 10KB of file.
          $handle = fopen($file_path, 'r');
          $content = fread($handle, 10240);
          fclose($handle);

          // Look for version patterns specific to PQINA Tick.
          $patterns = [
            '/\*\s+@pqina\/tick\s+v?([0-9]+\.[0-9]+(?:\.[0-9]+)?)/',
            '/\*\s+Tick\s+v?([0-9]+\.[0-9]+(?:\.[0-9]+)?)/',
            '/\*\s+@version\s+([0-9]+\.[0-9]+(?:\.[0-9]+)?)/',
            '/Tick\.version\s*=\s*["\']([^"\']+)["\']/',
            '/TICK_VERSION\s*=\s*["\']([0-9]+\.[0-9]+(?:\.[0-9]+)?)["\']/',
          ];

          foreach ($patterns as $pattern) {
            if (preg_match($pattern, $content, $matches)) {
              $this->logger->info('PQINA Tick version detected: @version from @file', [
                '@version' => $matches[1],
                '@file' => $js_file,
              ]);
              return $this->normalizeVersion($matches[1]);
            }
          }
        }
        catch (\Exception $e) {
          $this->logger->error('Error reading Tick file @file: @message', [
            '@file' => $js_file,
            '@message' => $e->getMessage(),
          ]);
        }
      }
    }

    // Check package.json for @pqina/tick.
    $package_file = $base_path . '/package.json';
    if (file_exists($package_file)) {
      try {
        $content = file_get_contents($package_file);
        $package_data = json_decode($content, TRUE);

        if (json_last_error() === JSON_ERROR_NONE) {
          // Check if it's the right package.
          if (isset($package_data['name']) &&
              ($package_data['name'] === '@pqina/tick' || $package_data['name'] === 'tick')) {
            if (!empty($package_data['version'])) {
              return $this->normalizeVersion($package_data['version']);
            }
          }
        }
      }
      catch (\Exception $e) {
        $this->logger->warning('Could not read Tick package.json', [
          '@error' => $e->getMessage(),
        ]);
      }
    }

    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  protected function resolveRequiredExtensions(array $library_config, array $config): array {
    $extensions = [];

    // Get view type from library configuration.
    $view_type = $library_config['view_type'] ?? 'text';

    // Map view types to required extensions.
    switch ($view_type) {
      case 'boom':
        $extensions[] = 'view_boom';
        break;

      case 'dots':
        $extensions[] = 'view_dots';
        // Also need font extension for dots view.
        $font = $library_config['font'] ?? 'highres';
        $extensions[] = 'font_' . $font;
        break;

      case 'line':
        $extensions[] = 'view_line';
        break;

      case 'swap':
        $extensions[] = 'view_swap';
        break;

      case 'text':
      default:
        // Text view needs no extensions.
        break;
    }

    return $extensions;
  }

  /**
   * {@inheritdoc}
   */
  protected function buildExtensionLibraryName(string $extension_id, array $config): ?string {
    // Check if this is a valid Tick extension.
    if (!isset(self::EXTENSIONS[$extension_id])) {
      return NULL;
    }

    // Remove view_ and font_ prefixes to match library names.
    $library_suffix = $extension_id;
    if (str_starts_with($library_suffix, 'view_')) {
      $library_suffix = substr($library_suffix, 5);
    }

    // Build the library name: tick_<suffix>(_cdn)(.min).
    $library_name = 'tick_' . $library_suffix;

    if ($config['method'] === 'cdn') {
      $library_name .= '_cdn';
    }

    if ($config['variant']) {
      $library_name .= '.min';
    }

    return $library_name;
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array &$form, FormStateInterface $form_state, array $default_values = []): void {
    // First, build the common fields from parent.
    parent::buildConfigurationForm($form, $form_state, $default_values);

    // View type selection.
    $form['library_specific']['view_type'] = [
      '#type' => 'select',
      '#title' => $this->t('View Type'),
      '#description' => $this->t('Select the Tick display style.'),
      '#options' => [
        'text' => $this->t('Text (simple text display)'),
        'swap' => $this->t('Swap (animated text swapping)'),
        'dots' => $this->t('Dots (dot matrix display)'),
        'line' => $this->t('Line (progress bar)'),
        'boom' => $this->t('Boom (with sound effects)'),
      ],
      '#default_value' => $this->getConfigValue($default_values, 'view_type', 'text'),
      '#required' => TRUE,
    ];

    // Font selection for dots view.
    $form['library_specific']['font'] = [
      '#type' => 'select',
      '#title' => $this->t('Font Resolution'),
      '#description' => $this->t('Select font resolution for dots view.'),
      '#options' => [
        'highres' => $this->t('High Resolution (5x7 matrix)'),
        'lowres' => $this->t('Low Resolution (3x5 matrix)'),
      ],
      '#default_value' => $this->getConfigValue($default_values, 'font', 'highres'),
      '#states' => [
        'visible' => [
          ':input[name$="[view_type]"]' => ['value' => 'dots'],
        ],
      ],
    ];

    // Transform pipeline configuration.
    $form['library_specific']['transforms'] = [
      '#type' => 'details',
      '#title' => $this->t('Value Transforms'),
      '#description' => $this->t('Configure value transformation pipeline.'),
      '#open' => FALSE,
    ];

    $form['library_specific']['transforms']['enable_transforms'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Enable value transforms'),
      '#description' => $this->t('Apply transformations to counter values.'),
      '#default_value' => $this->getConfigValue($default_values, 'enable_transforms', FALSE),
    ];

    $form['library_specific']['transforms']['transform_chain'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Transform Chain'),
      '#description' => $this->t('Enter transform chain (e.g., "pad(2) -> upper").'),
      '#default_value' => $this->getConfigValue($default_values, 'transform_chain', ''),
      '#states' => [
        'visible' => [
          ':input[name$="[enable_transforms]"]' => ['checked' => TRUE],
        ],
      ],
    ];

    // Transition configuration.
    $form['library_specific']['transitions'] = [
      '#type' => 'details',
      '#title' => $this->t('Transitions'),
      '#description' => $this->t('Configure animation transitions.'),
      '#open' => FALSE,
    ];

    $form['library_specific']['transitions']['transition_type'] = [
      '#type' => 'select',
      '#title' => $this->t('Transition Type'),
      '#description' => $this->t('Select the transition animation.'),
      '#options' => [
        'none' => $this->t('None'),
        'crossfade' => $this->t('Crossfade'),
        'swap' => $this->t('Swap'),
        'revolve' => $this->t('Revolve'),
        'zoom' => $this->t('Zoom'),
      ],
      '#default_value' => $this->getConfigValue($default_values, 'transition_type', 'crossfade'),
    ];

    $form['library_specific']['transitions']['transition_duration'] = [
      '#type' => 'number',
      '#title' => $this->t('Transition Duration'),
      '#description' => $this->t('Duration in milliseconds.'),
      '#default_value' => $this->getConfigValue($default_values, 'transition_duration', 500),
      '#min' => 0,
      '#max' => 5000,
      '#step' => 100,
      '#field_suffix' => $this->t('ms'),
    ];

    // Dots view specific settings.
    $form['library_specific']['dots_settings'] = [
      '#type' => 'details',
      '#title' => $this->t('Dots View Settings'),
      '#description' => $this->t('Settings for dot matrix display.'),
      '#open' => FALSE,
      '#states' => [
        'visible' => [
          ':input[name$="[view_type]"]' => ['value' => 'dots'],
        ],
      ],
    ];

    $form['library_specific']['dots_settings']['dot_color'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Dot Color'),
      '#description' => $this->t('CSS color for dots (e.g., #333, rgb(0,0,0)).'),
      '#default_value' => $this->getConfigValue($default_values, 'dot_color', 'auto'),
      '#size' => 20,
    ];

    $form['library_specific']['dots_settings']['dot_shape'] = [
      '#type' => 'select',
      '#title' => $this->t('Dot Shape'),
      '#description' => $this->t('Shape of the dots.'),
      '#options' => [
        'auto' => $this->t('Auto (from CSS)'),
        'square' => $this->t('Square'),
        'circle' => $this->t('Circle'),
      ],
      '#default_value' => $this->getConfigValue($default_values, 'dot_shape', 'auto'),
    ];

    $form['library_specific']['dots_settings']['dot_update_delay'] = [
      '#type' => 'number',
      '#title' => $this->t('Dot Update Delay'),
      '#description' => $this->t('Delay between dot updates in milliseconds.'),
      '#default_value' => $this->getConfigValue($default_values, 'dot_update_delay', 10),
      '#min' => 0,
      '#max' => 100,
      '#field_suffix' => $this->t('ms'),
    ];

    // Line view specific settings.
    $form['library_specific']['line_settings'] = [
      '#type' => 'details',
      '#title' => $this->t('Line View Settings'),
      '#description' => $this->t('Settings for progress bar display.'),
      '#open' => FALSE,
      '#states' => [
        'visible' => [
          ':input[name$="[view_type]"]' => ['value' => 'line'],
        ],
      ],
    ];

    $form['library_specific']['line_settings']['line_orientation'] = [
      '#type' => 'select',
      '#title' => $this->t('Line Orientation'),
      '#description' => $this->t('Orientation of the progress bar.'),
      '#options' => [
        'horizontal' => $this->t('Horizontal'),
        'vertical' => $this->t('Vertical'),
      ],
      '#default_value' => $this->getConfigValue($default_values, 'line_orientation', 'horizontal'),
    ];

    $form['library_specific']['line_settings']['line_flip'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Flip direction'),
      '#description' => $this->t('Reverse the fill direction.'),
      '#default_value' => $this->getConfigValue($default_values, 'line_flip', FALSE),
    ];

    $form['library_specific']['line_settings']['fill_color'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Fill Color'),
      '#description' => $this->t('Color of the progress fill.'),
      '#default_value' => $this->getConfigValue($default_values, 'fill_color', '#333'),
      '#size' => 20,
    ];

    $form['library_specific']['line_settings']['rail_color'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Rail Color'),
      '#description' => $this->t('Color of the progress rail/background.'),
      '#default_value' => $this->getConfigValue($default_values, 'rail_color', '#eee'),
      '#size' => 20,
    ];

    // Boom view specific settings.
    $form['library_specific']['boom_settings'] = [
      '#type' => 'details',
      '#title' => $this->t('Boom View Settings'),
      '#description' => $this->t('Settings for sound effects.'),
      '#open' => FALSE,
      '#states' => [
        'visible' => [
          ':input[name$="[view_type]"]' => ['value' => 'boom'],
        ],
      ],
    ];

    $form['library_specific']['boom_settings']['sample_url'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Sound Sample URL'),
      '#description' => $this->t('URL to the sound file (.mp3, .wav).'),
      '#default_value' => $this->getConfigValue($default_values, 'sample_url', ''),
      '#maxlength' => 255,
    ];

    $form['library_specific']['boom_settings']['volume'] = [
      '#type' => 'number',
      '#title' => $this->t('Volume'),
      '#description' => $this->t('Sound volume level.'),
      '#default_value' => $this->getConfigValue($default_values, 'volume', 0.5),
      '#min' => 0,
      '#max' => 1,
      '#step' => 0.1,
    ];

    // Swap view specific settings.
    $form['library_specific']['swap_settings'] = [
      '#type' => 'details',
      '#title' => $this->t('Swap View Settings'),
      '#description' => $this->t('Settings for swap animation.'),
      '#open' => FALSE,
      '#states' => [
        'visible' => [
          ':input[name$="[view_type]"]' => ['value' => 'swap'],
        ],
      ],
    ];

    $form['library_specific']['swap_settings']['transition_direction'] = [
      '#type' => 'select',
      '#title' => $this->t('Transition Direction'),
      '#description' => $this->t('Direction of the swap animation.'),
      '#options' => [
        'forward' => $this->t('Forward'),
        'reverse' => $this->t('Reverse'),
        'detect' => $this->t('Auto-detect'),
      ],
      '#default_value' => $this->getConfigValue($default_values, 'transition_direction', 'forward'),
    ];

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

    $form['library_specific']['advanced']['update_interval'] = [
      '#type' => 'number',
      '#title' => $this->t('Update Interval'),
      '#description' => $this->t('Update interval in milliseconds.'),
      '#default_value' => $this->getConfigValue($default_values, 'update_interval', 1000),
      '#min' => 100,
      '#max' => 10000,
      '#step' => 100,
      '#field_suffix' => $this->t('ms'),
    ];

    $form['library_specific']['advanced']['cascade'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Enable cascade'),
      '#description' => $this->t('Cascade time units (e.g., 61 seconds = 1 minute 1 second).'),
      '#default_value' => $this->getConfigValue($default_values, 'cascade', TRUE),
    ];

    $form['library_specific']['advanced']['server_sync'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Server time sync'),
      '#description' => $this->t('Synchronize with server time instead of client time.'),
      '#default_value' => $this->getConfigValue($default_values, 'server_sync', FALSE),
    ];

    $form['library_specific']['advanced']['autostart'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Autostart'),
      '#description' => $this->t('Start counting automatically when loaded.'),
      '#default_value' => $this->getConfigValue($default_values, 'autostart', TRUE),
    ];

    $form['library_specific']['advanced']['show_credits'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Show credits'),
      '#description' => $this->t('Display "Powered by PQINA" credit link.'),
      '#default_value' => $this->getConfigValue($default_values, 'show_credits', FALSE),
    ];

    // Custom CSS class.
    $form['library_specific']['custom_class'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Custom CSS Class'),
      '#description' => $this->t('Additional CSS class for styling.'),
      '#default_value' => $this->getConfigValue($default_values, 'custom_class', ''),
      '#maxlength' => 128,
    ];

    // Layout configuration.
    $form['library_specific']['layout'] = [
      '#type' => 'select',
      '#title' => $this->t('Layout'),
      '#description' => $this->t('Counter layout alignment.'),
      '#options' => [
        'left' => $this->t('Left aligned'),
        'center' => $this->t('Center aligned'),
        'right' => $this->t('Right aligned'),
        'fit' => $this->t('Fit to container'),
      ],
      '#default_value' => $this->getConfigValue($default_values, 'layout', 'left'),
    ];

    // Presets for common countdown formats.
    $form['library_specific']['preset'] = [
      '#type' => 'select',
      '#title' => $this->t('Preset Format'),
      '#description' => $this->t('Use a preset countdown format.'),
      '#options' => [
        'custom' => $this->t('Custom (use Display Format)'),
        'full' => $this->t('Full (Years, Months, Days, Hours, Minutes, Seconds)'),
        'extended' => $this->t('Extended (Days, Hours, Minutes, Seconds)'),
        'simple' => $this->t('Simple (Hours, Minutes, Seconds)'),
        'minimal' => $this->t('Minimal (Minutes, Seconds)'),
        'days_only' => $this->t('Days Only'),
        'hours_only' => $this->t('Hours Only'),
      ],
      '#default_value' => $this->getConfigValue($default_values, 'preset', 'custom'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void {
    // First validate common fields.
    parent::validateConfigurationForm($form, $form_state);

    // Get values from form state.
    $values = $form_state->getValues();

    // Handle nested structure from block forms.
    $source = $values;
    if (isset($values['library_specific']) && is_array($values['library_specific'])) {
      $source = $values['library_specific'];
    }

    // Get view_type with default value.
    $view_type = $source['view_type'] ?? 'text';

    // Validate color values based on view type.
    if ($view_type === 'dots') {
      $dot_color = $source['dots_settings']['dot_color'] ?? $source['dot_color'] ?? 'auto';
      if ($dot_color !== 'auto' && !$this->isValidColor($dot_color)) {
        $form_state->setErrorByName(
          'library_specific][dots_settings][dot_color',
          $this->t('Invalid color format for dot color. Use hex, rgb, rgba, or named colors.')
        );
      }
    }

    if ($view_type === 'line') {
      $fill_color = $source['line_settings']['fill_color'] ?? $source['fill_color'] ?? '#333';
      $rail_color = $source['line_settings']['rail_color'] ?? $source['rail_color'] ?? '#eee';

      if (!$this->isValidColor($fill_color)) {
        $form_state->setErrorByName(
          'library_specific][line_settings][fill_color',
          $this->t('Invalid color format for fill color. Use hex, rgb, rgba, or named colors.')
        );
      }

      if (!$this->isValidColor($rail_color)) {
        $form_state->setErrorByName(
          'library_specific][line_settings][rail_color',
          $this->t('Invalid color format for rail color. Use hex, rgb, rgba, or named colors.')
        );
      }
    }

    // Validate boom view settings.
    if ($view_type === 'boom') {
      $sample_url = $source['boom_settings']['sample_url'] ?? $source['sample_url'] ?? '';
      if (!empty($sample_url) && !$this->isValidUrlOrPath($sample_url)) {
        $form_state->setErrorByName(
          'library_specific][boom_settings][sample_url',
          $this->t('Invalid sound sample URL or file path. Please provide a valid URL or existing file path.')
        );
      }

      $volume = (float) ($source['boom_settings']['volume'] ?? $source['volume'] ?? 0.5);
      if ($volume < 0 || $volume > 1) {
        $form_state->setErrorByName(
          'library_specific][boom_settings][volume',
          $this->t('Volume must be between 0 and 1.')
        );
      }
    }

    // Validate transform chain if enabled.
    $transforms_enabled = (bool) ($source['transforms']['enable_transforms'] ?? $source['enable_transforms'] ?? FALSE);
    $transform_chain = $source['transforms']['transform_chain'] ?? $source['transform_chain'] ?? '';

    if ($transforms_enabled && !empty($transform_chain)) {
      if (!preg_match('/^[a-z0-9()_\->\s,]+$/i', $transform_chain)) {
        $form_state->setErrorByName(
          'library_specific][transforms][transform_chain',
          $this->t('Transform chain contains invalid characters. Only letters, numbers, parentheses, underscores, arrows, commas, and spaces are allowed.')
        );
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getDefaultConfiguration(): array {
    // Get parent defaults then add Tick-specific defaults.
    $defaults = parent::getDefaultConfiguration();

    // Add Tick-specific configuration defaults.
    $defaults['view_type'] = 'text';
    $defaults['font'] = 'highres';
    $defaults['enable_transforms'] = FALSE;
    $defaults['transform_chain'] = '';
    $defaults['transition_type'] = 'crossfade';
    $defaults['transition_duration'] = 500;
    $defaults['show_credits'] = FALSE;

    // Dots view defaults.
    $defaults['dot_color'] = 'auto';
    $defaults['dot_shape'] = 'auto';
    $defaults['dot_update_delay'] = 10;

    // Line view defaults.
    $defaults['line_orientation'] = 'horizontal';
    $defaults['line_flip'] = FALSE;
    $defaults['fill_color'] = '#333';
    $defaults['rail_color'] = '#eee';

    // Boom view defaults.
    $defaults['sample_url'] = '';
    $defaults['volume'] = 0.5;

    // Swap view defaults.
    $defaults['transition_direction'] = 'forward';

    // Advanced settings defaults.
    $defaults['update_interval'] = 1000;
    $defaults['cascade'] = TRUE;
    $defaults['server_sync'] = FALSE;
    $defaults['autostart'] = TRUE;
    $defaults['custom_class'] = '';
    $defaults['layout'] = 'left';
    $defaults['preset'] = 'custom';
    $defaults['format'] = ['d', 'h', 'm', 's'];

    return $defaults;
  }

  /**
   * {@inheritdoc}
   */
  public function getJavaScriptSettings(array $configuration): array {
    // Get parent settings then add Tick-specific settings.
    $settings = parent::getJavaScriptSettings($configuration);

    // Core Tick settings.
    $settings['view_type'] = $this->getConfigValue($configuration, 'view_type', 'text');
    $settings['interval'] = (int) $this->getConfigValue($configuration, 'update_interval', 1000);
    $settings['cascade'] = (bool) $this->getConfigValue($configuration, 'cascade', TRUE);
    $settings['server'] = (bool) $this->getConfigValue($configuration, 'server_sync', FALSE);
    $settings['autostart'] = (bool) $this->getConfigValue($configuration, 'autostart', TRUE);
    $settings['layout'] = $this->getConfigValue($configuration, 'layout', 'left');

    // Credits configuration.
    $settings['show_credits'] = (bool) $this->getConfigValue($configuration, 'show_credits', FALSE);

    // Custom CSS class.
    $custom_class = $this->getConfigValue($configuration, 'custom_class', '');
    if (!empty($custom_class)) {
      $settings['custom_class'] = $custom_class;
    }

    // Transform configuration.
    $settings['enable_transforms'] = (bool) $this->getConfigValue($configuration, 'enable_transforms', FALSE);
    if ($settings['enable_transforms']) {
      $transform_chain = $this->getConfigValue($configuration, 'transform_chain', '');
      if (!empty($transform_chain)) {
        $settings['transform_chain'] = $transform_chain;
      }
    }

    // Apply preset format if selected.
    $preset = $this->getConfigValue($configuration, 'preset', 'custom');
    if ($preset !== 'custom') {
      $settings['format'] = $this->getPresetFormat($preset);
    }
    else {
      // Ensure format is always an array.
      $format = $this->getConfigValue($configuration, 'format', ['d', 'h', 'm', 's']);
      if (is_string($format)) {
        $format = array_map('trim', explode(',', $format));
      }
      $settings['format'] = $format;
    }

    // Transition configuration.
    $transition_type = $this->getConfigValue($configuration, 'transition_type', 'crossfade');
    if ($transition_type !== 'none') {
      $settings['transition'] = [
        'name' => $transition_type,
        'duration' => (int) $this->getConfigValue($configuration, 'transition_duration', 500),
      ];
    }

    // View-specific settings based on selected view type.
    $view_type = $this->getConfigValue($configuration, 'view_type', 'text');

    switch ($view_type) {
      case 'dots':
        $settings['font'] = $this->getConfigValue($configuration, 'font', 'highres');
        $settings['dot_color'] = $this->getConfigValue($configuration, 'dot_color', 'auto');
        $settings['dot_shape'] = $this->getConfigValue($configuration, 'dot_shape', 'auto');
        $settings['dot_update_delay'] = (int) $this->getConfigValue($configuration, 'dot_update_delay', 10);
        break;

      case 'line':
        $settings['line_orientation'] = $this->getConfigValue($configuration, 'line_orientation', 'horizontal');
        $settings['line_flip'] = (bool) $this->getConfigValue($configuration, 'line_flip', FALSE);
        $settings['fill_color'] = $this->getConfigValue($configuration, 'fill_color', '#333');
        $settings['rail_color'] = $this->getConfigValue($configuration, 'rail_color', '#eee');
        break;

      case 'boom':
        $sample_url = $this->getConfigValue($configuration, 'sample_url', '');
        if (!empty($sample_url)) {
          $settings['sample_url'] = $sample_url;
        }
        $settings['volume'] = (float) $this->getConfigValue($configuration, 'volume', 0.5);
        break;

      case 'swap':
        $settings['transition_direction'] = $this->getConfigValue($configuration, 'transition_direction', 'forward');
        break;
    }

    return $settings;
  }

  /**
   * Helper method to validate color values.
   *
   * @param string $color
   *   The color value to validate.
   *
   * @return bool
   *   TRUE if the color is valid, FALSE otherwise.
   */
  private function isValidColor(string $color): bool {
    $pattern = '/^(#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*(0|1|0\.\d+)\s*\)|[a-z]+)$/i';
    return preg_match($pattern, $color) === 1;
  }

  /**
   * Helper method to validate URL or file path.
   *
   * @param string $url
   *   The URL or path to validate.
   *
   * @return bool
   *   TRUE if valid URL or existing file path, FALSE otherwise.
   */
  private function isValidUrlOrPath(string $url): bool {
    if (filter_var($url, FILTER_VALIDATE_URL)) {
      return TRUE;
    }

    // Check if it's a valid file path.
    if (file_exists($url)) {
      return TRUE;
    }

    // Check if it's a relative path within the Drupal installation.
    $drupal_root = DRUPAL_ROOT;
    if (file_exists($drupal_root . '/' . ltrim($url, '/'))) {
      return TRUE;
    }

    return FALSE;
  }

  /**
   * Convert preset to format array with proper validation.
   *
   * @param string $preset
   *   The preset identifier.
   *
   * @return array
   *   The format array.
   */
  protected function getPresetFormat(string $preset): array {
    $formats = [
      'full' => ['y', 'M', 'd', 'h', 'm', 's'],
      'extended' => ['d', 'h', 'm', 's'],
      'simple' => ['h', 'm', 's'],
      'minimal' => ['m', 's'],
      'days_only' => ['d'],
      'hours_only' => ['h'],
    ];

    return $formats[$preset] ?? ['d', 'h', 'm', 's'];
  }

  /**
   * {@inheritdoc}
   */
  public function hasExtensions(): bool {
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function getAvailableExtensions(): array {
    return self::EXTENSIONS;
  }

}

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

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