countdown-8.x-1.8/src/Element/CountdownPreview.php
src/Element/CountdownPreview.php
<?php
namespace Drupal\countdown\Element;
/**
* @file
* Contains the CountdownPreview render element.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\RenderElement;
/**
* Provides a countdown preview render element.
*
* This element creates a live preview of the selected countdown library with
* all its configurations and extensions. It handles all preview logic
* internally without needing external adapters or renderers.
*
* @RenderElement("countdown_preview")
*/
class CountdownPreview extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#pre_render' => [
[$class, 'preRenderCountdownPreview'],
],
'#theme' => 'countdown_preview',
'#attributes' => [],
'#form_state' => NULL,
];
}
/**
* Pre-render callback for the countdown preview element.
*
* @param array $element
* The render element.
*
* @return array
* The modified render element.
*/
public static function preRenderCountdownPreview(array $element) {
// Ensure ID is set.
if (empty($element['#attributes']['id'])) {
$element['#attributes']['id'] = 'countdown-preview-wrapper';
}
// Get form state if available.
$form_state = $element['#form_state'] ?? NULL;
if (!$form_state) {
$element['#countdown_markup'] = [
'#markup' => '<div class="countdown-preview-empty">' . t('No preview available') . '</div>',
];
return $element;
}
// Get configuration and plugin manager.
$config = \Drupal::config('countdown.settings');
$plugin_manager = \Drupal::service('plugin.manager.countdown_library');
// Extract current values from form state.
$library = $form_state->getValue('library');
if (!$library) {
$library = $config->get('library');
}
// If no library or "none", show placeholder.
if (!$library || $library === 'none') {
$element['#countdown_markup'] = [
'#markup' => '<div class="countdown-preview-placeholder">' . t('Select a countdown library to see the preview.') . '</div>',
];
return $element;
}
// Get the plugin instance.
try {
$plugin = $plugin_manager->createInstance($library);
}
catch (\Exception $e) {
$element['#countdown_markup'] = [
'#markup' => '<div class="countdown-preview-error">' . t('Selected library plugin was not found.') . '</div>',
];
return $element;
}
// Build preview configuration from form state.
$preview_config = self::buildPreviewConfig($form_state, $config, $plugin);
// Build the countdown markup.
$target_date = date('Y-m-d H:i:s', strtotime('+1 day'));
$data_attributes = [
'class' => ['countdown-timer'],
'data-countdown-target' => $target_date,
'data-countdown-format' => 'dhms',
];
// Add library-specific data attributes.
if ($library === 'tick' && !empty($preview_config['extensions'])) {
// Find the active view extension.
$view = 'text';
foreach ($preview_config['extensions'] as $ext) {
if (strpos($ext, 'view_') === 0) {
$view = substr($ext, 5);
}
}
$data_attributes['data-tick-view'] = $view;
}
$element['#countdown_markup'] = [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => $data_attributes,
];
$element['#countdown_loading'] = [
'#markup' => '<div class="countdown-preview-loading">' . t('Loading preview...') . '</div>',
'#weight' => -1,
];
// Build attachments using the plugin's buildAttachments method.
$attachments = $plugin->buildAttachments($preview_config);
// Apply the attachments to the element.
if (!empty($attachments['#attached'])) {
$element['#attached'] = $attachments['#attached'];
}
// For plugins with extensions, ensure extension libraries are loaded.
if ($plugin->hasExtensions() && !empty($preview_config['extensions'])) {
self::attachExtensionLibraries($element, $plugin, $preview_config);
}
// Add admin styles.
$element['#attached']['library'][] = 'countdown/admin';
// Add cleanup behavior.
$element['#attached']['library'][] = 'countdown/preview-cleanup';
// Add debug info if enabled.
if ($preview_config['debug']) {
$element['#debug_info'] = [
'library' => $library,
'method' => $preview_config['method'],
'extensions' => $preview_config['extensions'] ?? [],
'timestamp' => time(),
];
\Drupal::logger('countdown')->debug('Preview attached for @lib via @method with extensions: @ext', [
'@lib' => $library,
'@method' => $preview_config['method'],
'@ext' => implode(', ', $preview_config['extensions'] ?? []),
]);
}
return $element;
}
/**
* Builds preview configuration from form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Config\ImmutableConfig $config
* The configuration object.
* @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
* The plugin instance.
*
* @return array
* The preview configuration array.
*/
protected static function buildPreviewConfig(FormStateInterface $form_state, $config, $plugin): array {
// Extract variant value and convert to boolean.
$variant_value = $form_state->getValue('variant');
if ($variant_value === NULL) {
$variant_value = $config->get('build.variant');
}
// Build the configuration array.
$preview_config = [
'method' => $form_state->getValue('method') ?: $config->get('method') ?: 'local',
'variant' => (bool) $variant_value,
'cdn_provider' => $form_state->getValue('cdn_provider') ?: $config->get('cdn.provider') ?: 'jsdelivr',
'rtl' => (bool) ($form_state->getValue('rtl') ?? $config->get('rtl')),
'debug' => (bool) ($form_state->getValue('debug_mode') ?? $config->get('debug_mode')),
'extensions' => [],
];
// Get extensions if the plugin supports them.
if ($plugin->hasExtensions()) {
$preview_config['extensions'] = self::resolveActiveExtensions($form_state, $plugin, $config);
}
return $preview_config;
}
/**
* Resolves active extensions from form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
* The plugin instance.
* @param \Drupal\Core\Config\ImmutableConfig $config
* The configuration object.
*
* @return array
* Array of active extension IDs.
*/
protected static function resolveActiveExtensions(FormStateInterface $form_state, $plugin, $config): array {
$library_id = $plugin->getPluginId();
$active_extensions = [];
// Check triggering element for extension selection.
$trigger = $form_state->getTriggeringElement();
if ($trigger && !empty($trigger['#parents'])) {
$last = end($trigger['#parents']);
// If it's an extension that was triggered, handle it.
$available = $plugin->getAvailableExtensions();
if (isset($available[$last])) {
// For view extensions, only keep one active.
if (strpos($last, 'view_') === 0) {
// Clear other view extensions.
foreach ($available as $ext_id => $ext_info) {
if (strpos($ext_id, 'view_') === 0 && $ext_id !== $last) {
// Skip this view extension.
continue;
}
$input = $form_state->getUserInput();
if (!empty($input[$ext_id])) {
$active_extensions[] = $ext_id;
}
}
}
else {
// For non-view extensions, just check all inputs.
$input = $form_state->getUserInput();
foreach ($available as $ext_id => $ext_info) {
if (!empty($input[$ext_id])) {
$active_extensions[] = $ext_id;
}
}
}
}
}
// If no extensions from triggering element, check all user input.
if (empty($active_extensions)) {
$input = $form_state->getUserInput();
if (is_array($input)) {
$available = $plugin->getAvailableExtensions();
$view_found = FALSE;
foreach ($available as $ext_id => $ext_info) {
if (!empty($input[$ext_id])) {
// For view extensions, only keep the last one.
if (strpos($ext_id, 'view_') === 0) {
if (!$view_found) {
$active_extensions[] = $ext_id;
$view_found = TRUE;
}
}
else {
// Non-view extensions can all be active.
$active_extensions[] = $ext_id;
}
}
}
}
}
// Fall back to saved extensions if nothing selected.
if (empty($active_extensions)) {
$saved_extensions = $config->get("file_assets.extensions.{$library_id}") ?: [];
if (!empty($saved_extensions)) {
$active_extensions = $saved_extensions;
}
}
return $active_extensions;
}
/**
* Attaches extension libraries for plugins that support them.
*
* @param array &$element
* The render element to attach libraries to.
* @param \Drupal\countdown\Plugin\CountdownLibraryPluginInterface $plugin
* The plugin instance.
* @param array $preview_config
* The preview configuration.
*/
protected static function attachExtensionLibraries(array &$element, $plugin, array $preview_config): void {
$library_id = $plugin->getPluginId();
// Map extension IDs to library names based on plugin conventions.
foreach ($preview_config['extensions'] as $ext_id) {
// Build the library name for this extension.
$lib_name = 'countdown/' . $library_id . '_';
// Handle different extension naming patterns.
if (strpos($ext_id, 'view_') === 0) {
// View extensions: view_dots -> tick_dots.
$lib_name .= substr($ext_id, 5);
}
elseif (strpos($ext_id, 'font_') === 0) {
// Font extensions: font_highres -> tick_font_highres.
$lib_name .= 'font_' . substr($ext_id, 5);
}
else {
// Direct mapping for other extensions.
$lib_name .= $ext_id;
}
// Add CDN suffix if using CDN method.
if ($preview_config['method'] === 'cdn') {
$lib_name .= '_cdn';
}
// Add minified suffix if using production variant.
if ($preview_config['variant']) {
$lib_name .= '.min';
}
// Check if library exists before attaching.
$library_discovery = \Drupal::service('library.discovery');
$library_parts = explode('/', $lib_name);
if (count($library_parts) === 2) {
$library_definition = $library_discovery->getLibraryByName($library_parts[0], $library_parts[1]);
if ($library_definition) {
$element['#attached']['library'][] = $lib_name;
}
}
}
// Special handling for specific library requirements.
if ($library_id === 'tick') {
// Dots view requires font_highres.
if (in_array('view_dots', $preview_config['extensions'])) {
$font_lib = 'countdown/tick_font_highres';
if ($preview_config['method'] === 'cdn') {
$font_lib .= '_cdn';
}
if ($preview_config['variant']) {
$font_lib .= '.min';
}
$element['#attached']['library'][] = $font_lib;
}
}
// Ensure extensions are passed to drupalSettings.
if (!isset($element['#attached']['drupalSettings'])) {
$element['#attached']['drupalSettings'] = [];
}
if (!isset($element['#attached']['drupalSettings']['countdown'])) {
$element['#attached']['drupalSettings']['countdown'] = [];
}
// Pass extensions to the library namespace.
$element['#attached']['drupalSettings']['countdown'][$library_id] = [
'extensions' => $preview_config['extensions'],
'debug' => $preview_config['debug'],
];
}
}
