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

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

declare(strict_types=1);

namespace Drupal\countdown\Plugin\CountdownLibrary;

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

/**
 * FlipClock library plugin implementation.
 *
 * FlipClock is a proper abstract object oriented clock and counter library
 * with a realistic flip animation effect. It provides various clock faces
 * and countdown/countup functionality. This plugin supports both v0.7.x
 * (jQuery-based) and v0.10.x (standalone ES6) versions.
 *
 * @CountdownLibrary(
 *   id = "flipclock",
 *   label = @Translation("FlipClock"),
 *   description = @Translation("Clock and counter with realistic flip effect"),
 *   type = "external",
 *   homepage = "https://flipclockjs.com",
 *   repository = "https://github.com/objectivehtml/FlipClock",
 *   version = "0.7.7",
 *   npm_package = "flipclock",
 *   folder_names = {
 *     "flipclock",
 *     "FlipClock",
 *     "flipclock.js",
 *     "FlipClock.js",
 *     "FlipClock-master",
 *     "objectivehtml-FlipClock"
 *   },
 *   init_function = "FlipClock",
 *   author = "Objective HTML, LLC",
 *   license = "MIT",
 *   dependencies = {},
 *   weight = 0,
 *   experimental = false,
 *   api_version = "1.0"
 * )
 */
final class FlipClock extends CountdownLibraryPluginBase {

  /**
   * Cache for detected library version.
   *
   * @var string|null|false
   */
  protected $detectedVersion = NULL;

  /**
   * Cache for version detection based on file structure.
   *
   * @var bool|null
   */
  protected ?bool $isModernStructure = NULL;

  /**
   * {@inheritdoc}
   */
  public function getAssetMap(): array {
    // Use file structure detection instead of version detection to avoid
    // recursion during library path discovery.
    if ($this->hasModernStructure()) {
      return [
        'local' => [
          'js' => [
            'development' => 'dist/flipclock.js',
            'production' => 'dist/flipclock.min.js',
          ],
          'css' => [
            'development' => 'dist/flipclock.css',
            'production' => 'dist/flipclock.css',
          ],
        ],
        'cdn' => [
          'jsdelivr' => [
            'js' => '//cdn.jsdelivr.net/npm/flipclock@0.10.8/dist/flipclock.min.js',
            'css' => '//cdn.jsdelivr.net/npm/flipclock@0.10.8/dist/flipclock.css',
          ],
          'unpkg' => [
            'js' => '//unpkg.com/flipclock@0.10.8/dist/flipclock.min.js',
            'css' => '//unpkg.com/flipclock@0.10.8/dist/flipclock.css',
          ],
        ],
      ];
    }

    // Version 0.7.x uses compiled directory.
    return [
      'local' => [
        'js' => [
          'development' => 'compiled/flipclock.js',
          'production' => 'compiled/flipclock.min.js',
        ],
        'css' => [
          'development' => 'compiled/flipclock.css',
          'production' => 'compiled/flipclock.css',
        ],
      ],
      'cdn' => [
        'cdnjs' => [
          'js' => '//cdnjs.cloudflare.com/ajax/libs/flipclock/0.7.8/flipclock.min.js',
          'css' => '//cdnjs.cloudflare.com/ajax/libs/flipclock/0.7.8/flipclock.css',
        ],
      ],
    ];
  }

  /**
   * Check if library has modern file structure without version detection.
   *
   * This method checks file structure to determine version without causing
   * recursion through getLibraryPath() calls.
   *
   * @return bool
   *   TRUE if modern structure (v0.10.x), FALSE for legacy (v0.7.x).
   */
  protected function hasModernStructure(): bool {
    if ($this->isModernStructure !== NULL) {
      return $this->isModernStructure;
    }

    // Check common library directories without using getLibraryPath().
    $possible_paths = [
      'libraries/flipclock',
      'libraries/FlipClock',
      'libraries/flipclock.js',
      'libraries/FlipClock.js',
    ];

    foreach ($possible_paths as $path) {
      $full_path = DRUPAL_ROOT . '/' . $path;
      if (is_dir($full_path)) {
        // Check for modern structure (dist directory).
        if (file_exists($full_path . '/dist/flipclock.js')) {
          $this->isModernStructure = TRUE;
          return TRUE;
        }
        // Check for legacy structure (compiled directory).
        if (file_exists($full_path . '/compiled/flipclock.js')) {
          $this->isModernStructure = FALSE;
          return FALSE;
        }
      }
    }

    // Default to legacy if structure cannot be determined.
    $this->isModernStructure = FALSE;
    return FALSE;
  }

  /**
   * Check if the installed version is 0.10.x or newer.
   *
   * This method is safe to use after library path is established.
   *
   * @return bool
   *   TRUE if version is 0.10.x or newer, FALSE otherwise.
   */
  protected function isModernVersion(): bool {
    // Only use this method after installation, not during discovery.
    if (!$this->isInstalled()) {
      return $this->hasModernStructure();
    }

    $version = $this->getInstalledVersion();
    if ($version) {
      return version_compare($version, '0.10.0', '>=');
    }

    // Fall back to structure check.
    return $this->hasModernStructure();
  }

  /**
   * {@inheritdoc}
   */
  public function getDependencies(): array {
    // Use structure check to avoid recursion.
    if (!$this->hasModernStructure()) {
      return ['core/jquery', 'core/drupal'];
    }

    return ['core/drupal'];
  }

  /**
   * {@inheritdoc}
   */
  protected function detectVersionCustom(string $base_path): ?string {
    // Check for version in main JS files.
    $files_to_check = [
      '/dist/flipclock.js',
      '/dist/flipclock.min.js',
      '/compiled/flipclock.js',
      '/compiled/flipclock.min.js',
      '/src/flipclock/js/flipclock.js',
    ];

    foreach ($files_to_check as $file) {
      $file_path = $base_path . $file;

      if (file_exists($file_path)) {
        try {
          // Read only first 10KB to avoid loading large files.
          $handle = fopen($file_path, 'r');
          $content = fread($handle, 10240);
          fclose($handle);

          // Check for ES6 class structure (v0.10.x).
          if (strpos($content, 'class FlipClock') !== FALSE ||
              strpos($content, '_createClass(FlipClock') !== FALSE ||
              strpos($content, 'FlipClock.DayCounter') !== FALSE) {
            // This is version 0.10.x or newer.
            if (preg_match('/version["\']?\s*[:=]\s*["\']([0-9]+\.[0-9]+\.[0-9]+)["\']/', $content, $matches)) {
              return $matches[1];
            }
            // If no version found but ES6 structure detected, assume 0.10.8.
            return '0.10.8';
          }

          // Check for jQuery/Base.extend structure (v0.7.x).
          if (strpos($content, 'FlipClock.Base.extend') !== FALSE ||
              strpos($content, 'FlipClock.Factory') !== FALSE ||
              strpos($content, 'FlipClock.DailyCounterFace') !== FALSE) {
            // Look for version in v0.7.x format.
            if (preg_match('/version\s*:\s*["\']([0-9]+\.[0-9]+\.[0-9]+)["\']/', $content, $matches)) {
              return $matches[1];
            }
            // Check buildDate pattern in v0.7.x.
            if (preg_match('/buildDate\s*:\s*["\']([0-9]{4}-[0-9]{2}-[0-9]{2})["\']/', $content, $matches)) {
              // Map known build dates to versions.
              if ($matches[1] === '2014-12-12') {
                return '0.7.7';
              }
            }
            // Default to 0.7.8 for old version.
            return '0.7.8';
          }
        }
        catch (\Exception $e) {
          $this->logger->error('Error detecting FlipClock version: @message', [
            '@message' => $e->getMessage(),
          ]);
        }
      }
    }

    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function validateInstallation(string $path): bool {
    if (!parent::validateInstallation($path)) {
      return FALSE;
    }

    $path = ltrim($path, '/');
    $full_path = DRUPAL_ROOT . '/' . $path;

    // Check for version-specific structure.
    $modern_indicators = [
      '/dist/flipclock.js',
      '/dist/flipclock.css',
    ];

    $legacy_indicators = [
      '/compiled/flipclock.js',
      '/compiled/flipclock.css',
    ];

    $has_modern = FALSE;
    $has_legacy = FALSE;

    foreach ($modern_indicators as $indicator) {
      if (file_exists($full_path . $indicator)) {
        $has_modern = TRUE;
        break;
      }
    }

    foreach ($legacy_indicators as $indicator) {
      if (file_exists($full_path . $indicator)) {
        $has_legacy = TRUE;
        break;
      }
    }

    return $has_modern || $has_legacy;
  }

  /**
   * {@inheritdoc}
   */
  public function getRequiredFiles(): array {
    // Use structure check to avoid recursion.
    if ($this->hasModernStructure()) {
      return [
        'dist/flipclock.js',
        'dist/flipclock.css',
      ];
    }

    return [
      'compiled/flipclock.js',
      'compiled/flipclock.css',
    ];
  }

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

    // Use safe version detection for form building.
    $is_modern = $this->isModernVersion();

    // Build clock face options based on version.
    if ($is_modern) {
      // Version 0.10.x faces - uses 'face' key.
      $clock_faces = [
        'DayCounter' => $this->t('Daily Counter (Days, Hours, Minutes, Seconds)'),
        'HourCounter' => $this->t('Hourly Counter (Hours, Minutes, Seconds)'),
        'MinuteCounter' => $this->t('Minute Counter (Minutes, Seconds)'),
        'Counter' => $this->t('Simple Counter'),
        'TwentyFourHourClock' => $this->t('24-Hour Clock'),
        'TwelveHourClock' => $this->t('12-Hour Clock'),
        'WeekCounter' => $this->t('Weekly Counter'),
        'YearCounter' => $this->t('Yearly Counter'),
      ];
      $default_face = 'DayCounter';
      $face_key = 'face';
    }
    else {
      // Version 0.7.x faces - uses 'clockFace' key.
      $clock_faces = [
        'DailyCounter' => $this->t('Daily Counter (Days, Hours, Minutes, Seconds)'),
        'HourlyCounter' => $this->t('Hourly Counter (Hours, Minutes, Seconds)'),
        'MinuteCounter' => $this->t('Minute Counter (Minutes, Seconds)'),
        'Counter' => $this->t('Simple Counter'),
        'TwentyFourHourClock' => $this->t('24-Hour Clock'),
        'TwelveHourClock' => $this->t('12-Hour Clock'),
      ];
      $default_face = 'DailyCounter';
      $face_key = 'clockFace';
    }

    // Clock face selection with correct key.
    $form['library_specific'][$face_key] = [
      '#type' => 'select',
      '#title' => $this->t('Clock Face'),
      '#description' => $this->t('Select the type of clock or counter to display.'),
      '#options' => $clock_faces,
      '#default_value' => $this->getConfigValue($default_values, $face_key, $default_face),
      '#required' => TRUE,
    ];

    // Countdown mode configuration.
    $form['library_specific']['countdown'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Countdown Mode'),
      '#description' => $this->t('Enable countdown mode. When disabled, counts up.'),
      '#default_value' => $this->getConfigValue($default_values, 'countdown', TRUE),
    ];

    // Auto-start configuration.
    $form['library_specific']['autoStart'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Auto Start'),
      '#description' => $this->t('Automatically start the timer when loaded.'),
      '#default_value' => $this->getConfigValue($default_values, 'autoStart', TRUE),
    ];

    if ($is_modern) {
      // Version 0.10.x specific settings.
      $form['library_specific']['showSeconds'] = [
        '#type' => 'checkbox',
        '#title' => $this->t('Show Seconds'),
        '#description' => $this->t('Display seconds in the countdown.'),
        '#default_value' => $this->getConfigValue($default_values, 'showSeconds', TRUE),
      ];

      $form['library_specific']['showLabels'] = [
        '#type' => 'checkbox',
        '#title' => $this->t('Show Labels'),
        '#description' => $this->t('Display labels for time units.'),
        '#default_value' => $this->getConfigValue($default_values, 'showLabels', TRUE),
      ];

      // Animation rate for v0.10.x.
      $form['library_specific']['animationRate'] = [
        '#type' => 'number',
        '#title' => $this->t('Animation Rate'),
        '#description' => $this->t('The animation speed in milliseconds.'),
        '#default_value' => $this->getConfigValue($default_values, 'animationRate', 500),
        '#min' => 100,
        '#max' => 2000,
        '#step' => 100,
        '#field_suffix' => $this->t('ms'),
      ];

      // Minimum digits for v0.10.x.
      $form['library_specific']['minimumDigits'] = [
        '#type' => 'number',
        '#title' => $this->t('Minimum Digits'),
        '#description' => $this->t('Minimum number of digits to display.'),
        '#default_value' => $this->getConfigValue($default_values, 'minimumDigits', 0),
        '#min' => 0,
        '#max' => 4,
      ];
    }
    else {
      // Version 0.7.x specific settings.
      $form['library_specific']['showSeconds'] = [
        '#type' => 'checkbox',
        '#title' => $this->t('Show Seconds'),
        '#description' => $this->t('Display seconds in the countdown.'),
        '#default_value' => $this->getConfigValue($default_values, 'showSeconds', TRUE),
        '#states' => [
          'invisible' => [
            ':input[name="library_specific[clockFace]"]' => [
              ['value' => 'TwentyFourHourClock'],
              ['value' => 'TwelveHourClock'],
            ],
          ],
        ],
      ];

      // Show meridiem for 12-hour clock in v0.7.x.
      $form['library_specific']['showMeridium'] = [
        '#type' => 'checkbox',
        '#title' => $this->t('Show AM/PM'),
        '#description' => $this->t('Display AM/PM indicator for 12-hour clock.'),
        '#default_value' => $this->getConfigValue($default_values, 'showMeridium', TRUE),
        '#states' => [
          'visible' => [
            ':input[name="library_specific[clockFace]"]' => ['value' => 'TwelveHourClock'],
          ],
        ],
      ];

      // Language selection for v0.7.x.
      $form['library_specific']['language'] = [
        '#type' => 'select',
        '#title' => $this->t('Language'),
        '#description' => $this->t('Select the language for time unit labels.'),
        '#options' => [
          'english' => $this->t('English'),
          'spanish' => $this->t('Spanish'),
          'french' => $this->t('French'),
          'german' => $this->t('German'),
          'italian' => $this->t('Italian'),
          'dutch' => $this->t('Dutch'),
          'swedish' => $this->t('Swedish'),
          'russian' => $this->t('Russian'),
          'chinese' => $this->t('Chinese'),
          'portuguese' => $this->t('Portuguese'),
          'arabic' => $this->t('Arabic'),
          'finnish' => $this->t('Finnish'),
          'danish' => $this->t('Danish'),
          'latvian' => $this->t('Latvian'),
          'norwegian' => $this->t('Norwegian'),
        ],
        '#default_value' => $this->getConfigValue($default_values, 'language', 'english'),
      ];

      // Minimum digits for v0.7.x.
      $form['library_specific']['minimumDigits'] = [
        '#type' => 'number',
        '#title' => $this->t('Minimum Digits'),
        '#description' => $this->t('Minimum number of digits to display.'),
        '#default_value' => $this->getConfigValue($default_values, 'minimumDigits', 0),
        '#min' => 0,
        '#max' => 4,
      ];
    }

    // Custom CSS class configuration (both versions).
    $form['library_specific']['customClass'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Custom CSS Class'),
      '#description' => $this->t('Add custom CSS classes to the clock container.'),
      '#default_value' => $this->getConfigValue($default_values, 'customClass', ''),
      '#maxlength' => 255,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getDefaultConfiguration(): array {
    $defaults = parent::getDefaultConfiguration();

    // Use safe version detection.
    if ($this->isModernVersion()) {
      // V0.10.x defaults.
      $defaults['face'] = 'DayCounter';
      $defaults['showSeconds'] = TRUE;
      $defaults['showLabels'] = TRUE;
      $defaults['animationRate'] = 500;
      $defaults['minimumDigits'] = 0;
    }
    else {
      // V0.7.x defaults.
      $defaults['clockFace'] = 'DailyCounter';
      $defaults['showSeconds'] = TRUE;
      $defaults['showMeridium'] = TRUE;
      $defaults['language'] = 'english';
      $defaults['minimumDigits'] = 0;
    }

    // Common defaults.
    $defaults['countdown'] = TRUE;
    $defaults['autoStart'] = TRUE;
    $defaults['customClass'] = '';

    return $defaults;
  }

  /**
   * {@inheritdoc}
   */
  public function getJavaScriptSettings(array $configuration): array {
    $settings = parent::getJavaScriptSettings($configuration);

    // Use safe version detection for JavaScript settings.
    $is_modern = $this->isModernVersion();
    $settings['isModernVersion'] = $is_modern;

    // Try to get actual version if library is installed.
    if ($this->isInstalled()) {
      $version = $this->getInstalledVersion();
      $settings['libraryVersion'] = $version ?: ($is_modern ? '0.10.8' : '0.7.8');
    }
    else {
      $settings['libraryVersion'] = $is_modern ? '0.10.8' : '0.7.8';
    }

    // Common settings.
    $settings['countdown'] = (bool) $this->getConfigValue($configuration, 'countdown', TRUE);
    $settings['autoStart'] = (bool) $this->getConfigValue($configuration, 'autoStart', TRUE);
    $settings['showSeconds'] = (bool) $this->getConfigValue($configuration, 'showSeconds', TRUE);

    // Version-specific settings.
    if ($is_modern) {
      // V0.10.x uses 'face' key.
      $settings['face'] = $this->getConfigValue($configuration, 'face', 'DayCounter');
      $settings['showLabels'] = (bool) $this->getConfigValue($configuration, 'showLabels', TRUE);
      $settings['animationRate'] = (int) $this->getConfigValue($configuration, 'animationRate', 500);
      $settings['minimumDigits'] = (int) $this->getConfigValue($configuration, 'minimumDigits', 0);
    }
    else {
      // V0.7.x uses 'clockFace' key.
      $settings['clockFace'] = $this->getConfigValue($configuration, 'clockFace', 'DailyCounter');
      $settings['showMeridium'] = (bool) $this->getConfigValue($configuration, 'showMeridium', TRUE);
      $settings['language'] = $this->getConfigValue($configuration, 'language', 'english');
      $settings['minimumDigits'] = (int) $this->getConfigValue($configuration, 'minimumDigits', 0);
    }

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

    return $settings;
  }

}

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

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