countdown-8.x-1.8/src/Plugin/CountdownLibrary/FlipDown.php
src/Plugin/CountdownLibrary/FlipDown.php
<?php
declare(strict_types=1);
namespace Drupal\countdown\Plugin\CountdownLibrary;
use Drupal\Core\Form\FormStateInterface;
use Drupal\countdown\Plugin\CountdownLibraryPluginBase;
/**
* FlipDown library plugin implementation.
*
* FlipDown is a lightweight, performant flip-style countdown timer
* with a clean, modern design and smooth animations.
*
* @CountdownLibrary(
* id = "flipdown",
* label = @Translation("FlipDown"),
* description = @Translation("Lightweight flip-style timer with modern design"),
* type = "external",
* homepage = "https://pbutcher.uk/flipdown",
* repository = "https://github.com/PButcher/flipdown",
* version = "0.3.2",
* npm_package = "flipdown",
* folder_names = {
* "flipdown",
* "FlipDown",
* "flipdown.js",
* "flipdown-master",
* "PButcher-flipdown"
* },
* init_function = "FlipDown",
* author = "Peter Butcher",
* license = "MIT",
* dependencies = {
* "core/drupal",
* "core/once"
* },
* weight = 1,
* experimental = false,
* api_version = "1.0"
* )
*/
final class FlipDown extends CountdownLibraryPluginBase {
/**
* {@inheritdoc}
*/
public function getAssetMap(): array {
return [
'local' => [
'js' => [
'development' => 'dist/flipdown.js',
'production' => 'dist/flipdown.min.js',
],
'css' => [
'development' => 'dist/flipdown.css',
'production' => 'dist/flipdown.min.css',
],
],
'cdn' => [
'jsdelivr' => [
'js' => '//cdn.jsdelivr.net/npm/flipdown@0.3.2/dist/flipdown.min.js',
'css' => '//cdn.jsdelivr.net/npm/flipdown@0.3.2/dist/flipdown.min.css',
],
'unpkg' => [
'js' => '//unpkg.com/flipdown@0.3.2/dist/flipdown.min.js',
'css' => '//unpkg.com/flipdown@0.3.2/dist/flipdown.min.css',
],
],
];
}
/**
* {@inheritdoc}
*/
public function getDependencies(): array {
return [
'core/drupal',
'core/once',
];
}
/**
* {@inheritdoc}
*/
public function getHomepage(): ?string {
return 'https://pbutcher.uk/flipdown';
}
/**
* {@inheritdoc}
*/
protected function detectVersionCustom(string $base_path): ?string {
// Try multiple strategies to detect FlipDown version.
// Strategy 1: Check the main JS file for version information.
$js_files = [
'/dist/flipdown.js',
'/dist/flipdown.min.js',
'/src/flipdown.js',
'/example/js/flipdown/flipdown.js',
];
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 FlipDown.
$patterns = [
'/this\.version\s*=\s*["\']([0-9]+\.[0-9]+(?:\.[0-9]+)?)["\']/',
'/\*\s+FlipDown\s+v?([0-9]+\.[0-9]+(?:\.[0-9]+)?)/',
'/\*\s+@version\s+([0-9]+\.[0-9]+(?:\.[0-9]+)?)/',
'/version["\']?\s*[:=]\s*["\']([0-9]+\.[0-9]+(?:\.[0-9]+)?)["\']/',
'/FlipDown\.version\s*=\s*["\']([^"\']+)["\']/',
'/const\s+version\s*=\s*["\']([0-9]+\.[0-9]+(?:\.[0-9]+)?)["\']/',
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $content, $matches)) {
$this->logger->info('FlipDown version detected: @version from @file', [
'@version' => $matches[1],
'@file' => $js_file,
]);
return $this->normalizeVersion($matches[1]);
}
}
}
catch (\Exception $e) {
$this->logger->error('Error reading FlipDown file @file: @message', [
'@file' => $js_file,
'@message' => $e->getMessage(),
]);
}
}
}
// Strategy 2: Check CSS file for version comments.
$css_files = [
'/dist/flipdown.css',
'/dist/flipdown.min.css',
'/src/flipdown.css',
];
foreach ($css_files as $css_file) {
$file_path = $base_path . $css_file;
if (file_exists($file_path)) {
try {
$handle = fopen($file_path, 'r');
$content = fread($handle, 2048);
fclose($handle);
$pattern = '/FlipDown\s+v?([0-9]+\.[0-9]+(?:\.[0-9]+)?)/i';
if (preg_match($pattern, $content, $matches)) {
return $this->normalizeVersion($matches[1]);
}
}
catch (\Exception $e) {
$this->logger->warning('Could not read FlipDown CSS file: @file', [
'@file' => $css_file,
]);
}
}
}
// Strategy 3: Check documentation files.
$doc_files = [
'/README.md',
'/CHANGELOG.md',
'/VERSION',
'/.version',
];
foreach ($doc_files as $doc_file) {
$file_path = $base_path . $doc_file;
if (file_exists($file_path)) {
try {
$content = file_get_contents($file_path);
// Look for version patterns.
$pattern = '/(?:version|v)\s*([0-9]+\.[0-9]+(?:\.[0-9]+)?)/i';
if (preg_match($pattern, $content, $matches)) {
return $this->normalizeVersion($matches[1]);
}
}
catch (\Exception $e) {
$this->logger->warning('Could not read FlipDown documentation: @file', [
'@file' => $doc_file,
]);
}
}
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function validateInstallation(string $path): bool {
// First check parent validation.
if (!parent::validateInstallation($path)) {
return FALSE;
}
// Additional FlipDown-specific validation.
$path = ltrim($path, '/');
$full_path = DRUPAL_ROOT . '/' . $path;
// Check for FlipDown-specific indicators.
$flipdown_indicators = [
'/dist/flipdown.js',
'/dist/flipdown.css',
'/src/flipdown.js',
'/example/js/flipdown',
];
$found_indicator = FALSE;
foreach ($flipdown_indicators as $indicator) {
$check_path = $full_path . $indicator;
if (file_exists($check_path) || is_dir($check_path)) {
$found_indicator = TRUE;
break;
}
}
if (!$found_indicator) {
$this->logger->warning('FlipDown library structure not recognized at @path', [
'@path' => $path,
]);
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getRequiredFiles(): array {
return [
'dist/flipdown.js',
'dist/flipdown.css',
];
}
/**
* {@inheritdoc}
*/
public function getAlternativePaths(): array {
return [
[
'src/flipdown.js',
'src/flipdown.css',
],
[
'example/js/flipdown/flipdown.js',
'example/css/flipdown/flipdown.css',
],
[
'lib/flipdown.js',
'lib/flipdown.css',
],
];
}
/**
* {@inheritdoc}
*/
public function hasExtensions(): bool {
// FlipDown library doesn't have extensions.
return FALSE;
}
/**
* {@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);
// Now add FlipDown-specific fields to the library_specific fieldset.
// Theme selection based on library capabilities.
$form['library_specific']['theme'] = [
'#type' => 'select',
'#title' => $this->t('Theme'),
'#description' => $this->t('Select the FlipDown visual theme.'),
'#options' => [
'dark' => $this->t('Dark'),
'light' => $this->t('Light'),
],
'#default_value' => $this->getConfigValue($default_values, 'theme', 'dark'),
];
// Custom headings for each unit.
$form['library_specific']['headings'] = [
'#type' => 'details',
'#title' => $this->t('Unit Labels'),
'#description' => $this->t('Customize the labels displayed above each time unit.'),
'#open' => TRUE,
];
$default_headings = $this->getConfigValue($default_values, 'headings', $this->getDefaultHeadings());
$form['library_specific']['headings']['days'] = [
'#type' => 'textfield',
'#title' => $this->t('Days Label'),
'#default_value' => $default_headings[0] ?? 'Days',
'#size' => 20,
'#maxlength' => 20,
'#description' => $this->t('Label displayed above days counter.'),
];
$form['library_specific']['headings']['hours'] = [
'#type' => 'textfield',
'#title' => $this->t('Hours Label'),
'#default_value' => $default_headings[1] ?? 'Hours',
'#size' => 20,
'#maxlength' => 20,
'#description' => $this->t('Label displayed above hours counter.'),
];
$form['library_specific']['headings']['minutes'] = [
'#type' => 'textfield',
'#title' => $this->t('Minutes Label'),
'#default_value' => $default_headings[2] ?? 'Minutes',
'#size' => 20,
'#maxlength' => 20,
'#description' => $this->t('Label displayed above minutes counter.'),
];
$form['library_specific']['headings']['seconds'] = [
'#type' => 'textfield',
'#title' => $this->t('Seconds Label'),
'#default_value' => $default_headings[3] ?? 'Seconds',
'#size' => 20,
'#maxlength' => 20,
'#description' => $this->t('Label displayed above seconds counter.'),
];
// Enable custom CSS class for additional theming.
$form['library_specific']['custom_class'] = [
'#type' => 'textfield',
'#title' => $this->t('Custom CSS Class'),
'#description' => $this->t('Add a custom CSS class to the FlipDown container for additional styling.'),
'#default_value' => $this->getConfigValue($default_values, 'custom_class', ''),
'#size' => 40,
'#maxlength' => 100,
];
// Option to control rotor animation behavior.
$form['library_specific']['enable_animation'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable flip animations'),
'#description' => $this->t('Enable smooth flip animations when values change.'),
'#default_value' => $this->getConfigValue($default_values, 'enable_animation', TRUE),
];
// Option to show/hide specific time units.
$form['library_specific']['show_units'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Display Units'),
'#description' => $this->t('Select which time units to display in the countdown.'),
'#options' => [
'days' => $this->t('Days'),
'hours' => $this->t('Hours'),
'minutes' => $this->t('Minutes'),
'seconds' => $this->t('Seconds'),
],
'#default_value' => $this->getConfigValue($default_values, 'show_units', [
'days' => 'days',
'hours' => 'hours',
'minutes' => 'minutes',
'seconds' => 'seconds',
]),
];
// Option for responsive behavior.
$form['library_specific']['responsive'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable responsive mode'),
'#description' => $this->t('Automatically adjust the timer size based on container width.'),
'#default_value' => $this->getConfigValue($default_values, 'responsive', FALSE),
];
// Option to enable debug mode for development.
$form['library_specific']['debug_mode'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable debug mode'),
'#description' => $this->t('Log FlipDown initialization and events to browser console.'),
'#default_value' => $this->getConfigValue($default_values, 'debug_mode', FALSE),
];
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void {
// First validate common fields.
parent::validateConfigurationForm($form, $form_state);
// Validate theme selection.
$theme = $form_state->getValue(['library_specific', 'theme']);
if ($theme && !in_array($theme, ['dark', 'light'], TRUE)) {
$form_state->setError($form['library_specific']['theme'],
$this->t('Invalid theme selected.'));
}
// Validate that at least one time unit is selected.
$show_units = array_filter($form_state->getValue(['library_specific', 'show_units'], []));
if (empty($show_units)) {
$form_state->setError($form['library_specific']['show_units'],
$this->t('At least one time unit must be displayed.'));
}
// Validate custom CSS class format.
$custom_class = $form_state->getValue(['library_specific', 'custom_class']);
if ($custom_class && !preg_match('/^[a-zA-Z][a-zA-Z0-9\-_]*$/', $custom_class)) {
$form_state->setError($form['library_specific']['custom_class'],
$this->t('Custom CSS class must be a valid CSS class name.'));
}
// Validate heading labels are not empty.
$headings = $form_state->getValue(['library_specific', 'headings']);
foreach (['days', 'hours', 'minutes', 'seconds'] as $unit) {
if (isset($headings[$unit]) && trim($headings[$unit]) === '') {
$form_state->setError($form['library_specific']['headings'][$unit],
$this->t('@unit label cannot be empty.', ['@unit' => ucfirst($unit)]));
}
}
}
/**
* {@inheritdoc}
*/
public function getDefaultConfiguration(): array {
// Get parent defaults then add FlipDown-specific defaults.
$defaults = parent::getDefaultConfiguration();
$defaults['theme'] = 'dark';
$defaults['headings'] = $this->getDefaultHeadings();
$defaults['custom_class'] = '';
$defaults['enable_animation'] = TRUE;
$defaults['show_units'] = [
'days' => 'days',
'hours' => 'hours',
'minutes' => 'minutes',
'seconds' => 'seconds',
];
$defaults['responsive'] = FALSE;
$defaults['debug_mode'] = FALSE;
return $defaults;
}
/**
* Get default heading labels.
*
* @return array
* Array of default heading labels.
*/
protected function getDefaultHeadings(): array {
return ['Days', 'Hours', 'Minutes', 'Seconds'];
}
/**
* {@inheritdoc}
*/
public function getJavaScriptSettings(array $configuration): array {
// Get parent settings then add FlipDown-specific settings.
$settings = parent::getJavaScriptSettings($configuration);
// Build headings array from individual fields.
$headings = [
$configuration['headings']['days'] ?? 'Days',
$configuration['headings']['hours'] ?? 'Hours',
$configuration['headings']['minutes'] ?? 'Minutes',
$configuration['headings']['seconds'] ?? 'Seconds',
];
// Convert show_units checkboxes to array of selected keys.
// This is important: we send the selected units as an array.
$show_units = isset($configuration['show_units'])
? array_filter($configuration['show_units'])
: ['days' => 'days', 'hours' => 'hours', 'minutes' => 'minutes', 'seconds' => 'seconds'];
// Send as array of selected unit names for JavaScript.
$selected_units = [];
foreach ($show_units as $unit => $value) {
if ($value) {
$selected_units[] = $unit;
}
}
$settings['theme'] = $this->getConfigValue($configuration, 'theme', 'dark');
$settings['headings'] = $headings;
$settings['custom_class'] = $this->getConfigValue($configuration, 'custom_class', '');
$settings['enable_animation'] = $this->getConfigValue($configuration, 'enable_animation', TRUE);
// Send selected units as array for easier checking in JavaScript.
$settings['show_units'] = $selected_units;
$settings['responsive'] = $this->getConfigValue($configuration, 'responsive', FALSE);
$settings['debug_mode'] = $this->getConfigValue($configuration, 'debug_mode', FALSE);
return $settings;
}
}
