countdown-8.x-1.8/src/Plugin/CountdownLibrary/CoreTimer.php
src/Plugin/CountdownLibrary/CoreTimer.php
<?php
declare(strict_types=1);
namespace Drupal\countdown\Plugin\CountdownLibrary;
use Drupal\Core\Form\FormStateInterface;
use Drupal\countdown\Plugin\CountdownLibraryPluginBase;
/**
* Core countdown timer library plugin implementation.
*
* This is the built-in countdown timer that ships with the module.
* It provides a lightweight, high-performance countdown/countup timer
* with drift compensation and no external dependencies.
*
* @CountdownLibrary(
* id = "countdown",
* label = @Translation("Countdown Timer"),
* description = @Translation("Built-in timer with drift compensation"),
* type = "core",
* homepage = "https://drupal.org/project/countdown",
* repository = "https://git.drupalcode.org/project/countdown",
* version = "1.0.0",
* init_function = "CountdownTimer",
* author = "Drupal Community",
* license = "GPL-2.0+",
* dependencies = {
* "core/drupal",
* "core/drupalSettings",
* "core/once"
* },
* weight = -10,
* experimental = false,
* api_version = "1.0"
* )
*/
final class CoreTimer extends CountdownLibraryPluginBase {
/**
* {@inheritdoc}
*/
public function isInstalled(): bool {
// Core library is always installed.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getLibraryPath(): ?string {
// Core library files are in the module directory.
$module_handler = \Drupal::moduleHandler();
$module_path = $module_handler->getModule('countdown')->getPath();
return '/' . $module_path;
}
/**
* {@inheritdoc}
*/
public function validateInstallation(string $path): bool {
// Core timer validation checks for required files.
$module_handler = \Drupal::moduleHandler();
$module_path = $module_handler->getModule('countdown')->getPath();
// Check for required core timer files.
$required_files = [
'/js/lib/countdown.js',
'/js/countdown.integration.js',
'/css/countdown.timer.css',
];
foreach ($required_files as $file) {
$file_path = DRUPAL_ROOT . '/' . $module_path . $file;
if (!file_exists($file_path)) {
$this->logger->error('Core timer file missing: @file', [
'@file' => $file,
]);
return FALSE;
}
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function detectVersion(string $path): ?string {
// Core library version is fixed and matches the module version.
return $this->getRequiredVersion();
}
/**
* {@inheritdoc}
*/
protected function detectVersionCustom(string $base_path): ?string {
// Core library version is defined in the module info file.
$module_handler = \Drupal::moduleHandler();
$module = $module_handler->getModule('countdown');
if ($module) {
$info = \Drupal::service('extension.list.module')
->getExtensionInfo('countdown');
if (isset($info['version'])) {
// Remove Drupal version prefix if present.
$version = preg_replace('/^\d+\.x-/', '', $info['version']);
return $this->normalizeVersion($version);
}
}
// Fallback to default version.
return '1.0.0';
}
/**
* {@inheritdoc}
*/
public function getInstalledVersion(): ?string {
// Core library version is always the current version.
return $this->detectVersionCustom('');
}
/**
* {@inheritdoc}
*/
public function versionMeetsRequirements(): bool {
// Core library always meets requirements.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getAssetMap(): array {
// Core library assets are defined in countdown.libraries.yml.
// We return an empty map as the module handles core assets directly.
return [];
}
/**
* {@inheritdoc}
*/
public function getInitFunction(): ?string {
return 'CountdownTimer';
}
/**
* {@inheritdoc}
*/
public function getDependencies(): array {
return [
'core/drupal',
'core/drupalSettings',
'core/once',
];
}
/**
* {@inheritdoc}
*/
public function getHomepage(): ?string {
return 'https://drupal.org/project/countdown';
}
/**
* {@inheritdoc}
*/
public function getRequiredFiles(): array {
// Core library files are always present.
return [];
}
/**
* {@inheritdoc}
*/
public function getPossibleFolderNames(): array {
// Core library doesn't need folder discovery.
return [];
}
/**
* {@inheritdoc}
*/
public function getStatus(): array {
return [
'installed' => TRUE,
'version_status' => 'ok',
'messages' => [
$this->t('Core library is built-in and always available.'),
$this->t('Version: @version', [
'@version' => $this->getInstalledVersion(),
]),
$this->t('High-performance timer with drift compensation.'),
$this->t('No external dependencies required.'),
],
'severity' => 'info',
];
}
/**
* {@inheritdoc}
*/
public function isExperimental(): bool {
// Core library is stable.
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getWeight(): int {
// Core library has highest priority.
return -10;
}
/**
* {@inheritdoc}
*/
public function resetCache(): void {
// Core library doesn't need cache reset as it's always available.
// But we'll clear Drupal's library cache for consistency.
\Drupal::service('library.discovery')->clearCachedDefinitions();
}
/**
* {@inheritdoc}
*/
public function hasExtensions(): bool {
// Core library doesn't have extensions.
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getExtensionGroups(): array {
// No extension groups for core library.
return [];
}
/**
* {@inheritdoc}
*/
public function getAvailableExtensions(): array {
// No extensions for core library.
return [];
}
/**
* {@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 CoreTimer-specific fields to the library_specific fieldset.
$form['library_specific']['style'] = [
'#type' => 'select',
'#title' => $this->t('Display Style'),
'#description' => $this->t('Choose how to display the countdown.'),
'#options' => [
'default' => $this->t('Default (automatic)'),
'compact' => $this->t('Compact (minimal spacing)'),
'expanded' => $this->t('Expanded (with labels)'),
],
'#default_value' => $this->getConfigValue($default_values, 'style', 'default'),
];
// Add precision control for time display accuracy.
$form['library_specific']['precision'] = [
'#type' => 'select',
'#title' => $this->t('Time Precision'),
'#description' => $this->t('Select the precision level for time display.'),
'#options' => [
'milliseconds' => $this->t('Milliseconds (HH:MM:SS.mmm)'),
'hundredths' => $this->t('Hundredths (HH:MM:SS.mm)'),
'tenths' => $this->t('Tenths (HH:MM:SS.m)'),
'seconds' => $this->t('Seconds (HH:MM:SS)'),
'minutes' => $this->t('Minutes (HH:MM)'),
],
'#default_value' => $this->getConfigValue($default_values, 'precision', 'seconds'),
];
// Add checkbox to enable custom format.
$form['library_specific']['enable_custom_format'] = [
'#type' => 'checkbox',
'#title' => $this->t('Use custom format template'),
'#description' => $this->t('Enable to specify a custom time display format.'),
'#default_value' => !empty($this->getConfigValue($default_values, 'format_template', '')),
];
// Add custom format template field.
$form['library_specific']['format_template'] = [
'#type' => 'textfield',
'#title' => $this->t('Custom Format Template'),
'#description' => $this->t('Custom time format. Use: DD (days), HH (hours), MM (minutes), SS (seconds), mmm (milliseconds). Leave empty for automatic format based on precision.'),
'#default_value' => $this->getConfigValue($default_values, 'format_template', ''),
'#maxlength' => 64,
'#states' => [
'visible' => [
':input[name$="[enable_custom_format]"]' => ['checked' => TRUE],
],
],
];
// Create a performance settings group.
$form['library_specific']['performance'] = [
'#type' => 'details',
'#title' => $this->t('Performance Settings'),
'#description' => $this->t('Configure timer performance and accuracy settings.'),
'#open' => TRUE,
];
// Add auto-start option.
$form['library_specific']['performance']['auto_start'] = [
'#type' => 'checkbox',
'#title' => $this->t('Auto-start Timer'),
'#description' => $this->t('Automatically start the timer when the page loads.'),
'#default_value' => $this->getConfigValue($default_values, 'auto_start', TRUE),
];
// Add drift compensation toggle.
$form['library_specific']['performance']['drift_compensation'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable Drift Compensation'),
'#description' => $this->t('Automatically compensates for JavaScript timer drift to maintain accuracy. Recommended for long-running timers.'),
'#default_value' => $this->getConfigValue($default_values, 'drift_compensation', TRUE),
];
// Add initial offset field.
$form['library_specific']['performance']['initial_offset'] = [
'#type' => 'number',
'#title' => $this->t('Initial Offset'),
'#description' => $this->t('Start time offset in milliseconds. Useful for synchronization.'),
'#default_value' => $this->getConfigValue($default_values, 'initial_offset', 0),
'#min' => 0,
'#max' => 86400000,
'#step' => 100,
'#field_suffix' => $this->t('ms'),
];
// Create developer tools group (only for admins).
$current_user = \Drupal::currentUser();
if ($current_user->hasPermission('administer site configuration')) {
$form['library_specific']['developer'] = [
'#type' => 'details',
'#title' => $this->t('Developer Tools'),
'#description' => $this->t('Advanced tools for debugging and performance analysis.'),
'#open' => FALSE,
];
// Add benchmark mode toggle.
$form['library_specific']['developer']['benchmark_mode'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable Benchmark Mode'),
'#description' => $this->t('Collect performance metrics including drift statistics and timing accuracy.'),
'#default_value' => $this->getConfigValue($default_values, 'benchmark_mode', FALSE),
];
// Add debug mode toggle.
$form['library_specific']['developer']['debug_mode'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable Debug Mode'),
'#description' => $this->t('Output detailed timer information to the 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 custom format template if enabled.
$enable_custom = $form_state->getValue(['library_specific', 'enable_custom_format']);
$format_template = $form_state->getValue(['library_specific', 'format_template']);
if ($enable_custom && !empty($format_template)) {
// Check for valid format tokens only.
if (!preg_match('/^[DHMSm:\s\-\.\/]+$/', $format_template)) {
$form_state->setError(
$form['library_specific']['format_template'],
$this->t('Invalid format template. Use only: DD, D, HH, H, MM, M, SS, S, mmm, mm, m and separators.')
);
}
// Check if template contains at least one time token.
if (!preg_match('/(DD|D|HH|H|MM|M|SS|S|mmm|mm|m)/', $format_template)) {
$form_state->setError(
$form['library_specific']['format_template'],
$this->t('Format template must contain at least one time token.')
);
}
}
// Validate initial offset value.
$offset = $form_state->getValue(['library_specific', 'performance', 'initial_offset']);
if ($offset !== NULL && $offset !== '') {
if (!is_numeric($offset) || $offset < 0) {
$form_state->setError(
$form['library_specific']['performance']['initial_offset'],
$this->t('Initial offset must be a positive number.')
);
}
}
// Validate precision compatibility with format template.
$precision = $form_state->getValue(['library_specific', 'precision']);
if ($enable_custom && !empty($format_template) && $precision) {
// Check for incompatible combinations.
if ($precision === 'minutes' && preg_match('/(SS|S|mmm|mm|m)/', $format_template)) {
$form_state->setWarning(
$this->t('Format template contains seconds or milliseconds tokens, but precision is set to minutes. These values will not update.')
);
}
elseif ($precision === 'seconds' && preg_match('/(mmm|mm|m)/', $format_template)) {
$form_state->setWarning(
$this->t('Format template contains milliseconds tokens, but precision is set to seconds. These values will remain at zero.')
);
}
}
// Validate benchmark and debug modes are not both enabled in production.
$benchmark = $form_state->getValue(['library_specific', 'developer', 'benchmark_mode']);
$debug = $form_state->getValue(['library_specific', 'developer', 'debug_mode']);
if (($benchmark || $debug) && !\Drupal::service('config.factory')->get('system.performance')->get('debug')) {
$form_state->setWarning(
$this->t('Developer tools are enabled. Remember to disable them in production for better performance.')
);
}
}
/**
* {@inheritdoc}
*/
public function getDefaultConfiguration(): array {
// Get parent defaults then add CoreTimer-specific defaults.
$defaults = parent::getDefaultConfiguration();
$defaults['style'] = 'default';
$defaults['precision'] = 'seconds';
$defaults['format_template'] = '';
$defaults['enable_custom_format'] = FALSE;
$defaults['auto_start'] = TRUE;
$defaults['drift_compensation'] = TRUE;
$defaults['initial_offset'] = 0;
$defaults['benchmark_mode'] = FALSE;
$defaults['debug_mode'] = FALSE;
return $defaults;
}
/**
* {@inheritdoc}
*/
public function getJavaScriptSettings(array $configuration): array {
// Get parent settings then add CoreTimer-specific settings.
$settings = parent::getJavaScriptSettings($configuration);
// Add display style configuration.
$settings['style'] = $this->getConfigValue($configuration, 'style', 'default');
// Add precision setting for time accuracy.
$settings['precision'] = $this->getConfigValue($configuration, 'precision', 'seconds');
// Add drift compensation setting.
$settings['driftCompensation'] = (bool) $this->getConfigValue($configuration, 'drift_compensation', TRUE);
// Add auto-start configuration.
$settings['autoStart'] = (bool) $this->getConfigValue($configuration, 'auto_start', TRUE);
// Add initial offset if specified.
$offset = $this->getConfigValue($configuration, 'initial_offset', 0);
if ($offset > 0) {
$settings['offset'] = (int) $offset;
}
// Handle custom format template correctly.
// Check if we have a non-empty format_template value.
$format_template = $this->getConfigValue($configuration, 'format_template', '');
if (!empty($format_template)) {
// Add the format template to settings.
$settings['formatTemplate'] = $format_template;
// Also set a flag to indicate custom format is enabled.
$settings['useCustomFormat'] = TRUE;
}
// Add benchmark mode if enabled (admin only).
$benchmark = (bool) $this->getConfigValue($configuration, 'benchmark_mode', FALSE);
if ($benchmark) {
$settings['benchmark'] = TRUE;
}
// Add debug mode if enabled (admin only).
$debug = (bool) $this->getConfigValue($configuration, 'debug_mode', FALSE);
if ($debug) {
$settings['debug'] = TRUE;
}
// Add mode configuration for countdown vs countup.
$direction = $this->getConfigValue($configuration, 'direction', 'countdown');
$settings['mode'] = $direction === 'countup' ? 'countup' : 'countdown';
// Calculate and add start/target values based on configuration.
$target_date = $this->getConfigValue($configuration, 'target_date', '');
$timezone = $this->getConfigValue($configuration, 'timezone', date_default_timezone_get());
if (!empty($target_date)) {
// Convert target date to timestamp considering timezone.
$date = new \DateTime($target_date, new \DateTimeZone($timezone));
$now = new \DateTime('now', new \DateTimeZone($timezone));
$diff = $date->getTimestamp() - $now->getTimestamp();
if ($settings['mode'] === 'countdown') {
// For countdown, set the start time based on difference.
if ($diff > 0) {
$settings['start'] = $diff * 1000;
}
}
else {
// For countup, set the target time.
$settings['target'] = abs($diff) * 1000;
}
}
return $settings;
}
}
