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;
}
}
