vvjs-1.0.1/vvjs.module
vvjs.module
<?php
/**
* @file
* Provides the module implementation for vvjs.
*
* Filename: vvjs.module
* Website: https://www.flashwebcenter.com
* Description: template.
* Developer: Alaa Haddad https://www.alaahaddad.com.
*/
declare(strict_types=1);
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Markup;
use Drupal\views\ViewExecutable;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Template\Attribute;
use Drupal\vvjs\Plugin\views\style\ViewsVanillaJavascriptSlideshow;
use Drupal\vvjs\VvjsConstants;
/**
* Implements hook_help().
*/
function vvjs_help(string $route_name, RouteMatchInterface $route_match): ?string {
if ($route_name === 'help.page.vvjs') {
return _vvjs_helper_render_readme();
}
return NULL;
}
/**
* Helper function to render README.md with enhanced error handling.
*
* @return string
* The rendered content of README.md or appropriate fallback message.
*/
function _vvjs_helper_render_readme(): string {
$readme_path = __DIR__ . '/README.md';
try {
$text = file_get_contents($readme_path);
if ($text === FALSE) {
\Drupal::logger('vvjs')->warning('README.md file could not be read from path: @path', [
'@path' => $readme_path,
]);
return (string) t('README.md file not found.');
}
if (!\Drupal::moduleHandler()->moduleExists('markdown')) {
return '<pre>' . htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '</pre>';
}
return _vvjs_render_markdown($text);
}
catch (\Exception $e) {
\Drupal::logger('vvjs')->error('Error rendering README.md: @message', [
'@message' => $e->getMessage(),
]);
return (string) t('Error loading help documentation.');
}
}
/**
* Render markdown content using the markdown filter.
*
* @param string $text
* The markdown text to render.
*
* @return string
* The rendered HTML content.
*/
function _vvjs_render_markdown(string $text): string {
try {
$filter_manager = \Drupal::service('plugin.manager.filter');
$settings = \Drupal::config('markdown.settings')->getRawData();
$filter = $filter_manager->createInstance('markdown', ['settings' => $settings]);
return $filter->process($text, 'en')->getProcessedText();
}
catch (\Exception $e) {
\Drupal::logger('vvjs')->warning('Markdown processing failed: @message', [
'@message' => $e->getMessage(),
]);
return '<pre>' . htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '</pre>';
}
}
/**
* Implements hook_theme().
*/
function vvjs_theme(array $existing, string $type, string $theme, string $path): array {
return [
'views_view_vvjs_fields' => [
'variables' => [
'view' => NULL,
'options' => [],
'row' => NULL,
'field_alias' => NULL,
'attributes' => [],
'title_attributes' => [],
'content_attributes' => [],
'title_prefix' => [],
'title_suffix' => [],
'fields' => [],
],
'template' => 'views-view-vvjs-fields',
'path' => $path . '/templates',
],
'views_view_vvjs' => [
'variables' => [
'view' => NULL,
'rows' => [],
'options' => [],
],
'template' => 'views-view-vvjs',
'path' => $path . '/templates',
],
];
}
/**
* Implements hook_preprocess_HOOK() for views_view_vvjs.
*/
function template_preprocess_views_view_vvjs(array &$variables): void {
\Drupal::moduleHandler()->loadInclude('views', 'inc', 'views.theme');
$handler = $variables['view']->style_plugin ?? NULL;
if (!$handler) {
\Drupal::logger('vvjs')->error('Style plugin handler not found in view preprocessing.');
return;
}
$options = $handler->options ?? [];
$variables['list_attributes'] = _vvjs_build_data_attributes($options);
$variables['background_rgb'] = _vvjs_calculate_background_rgb($options);
$variables['settings'] = _vvjs_build_template_settings($handler, $options);
_vvjs_customize_row_theme_suggestions($variables);
template_preprocess_views_view_unformatted($variables);
}
/**
* Build data attributes array from slideshow options.
*
* @param array $options
* The slideshow configuration options.
*
* @return \Drupal\Core\Template\Attribute
* Attribute object containing data attributes.
*/
function _vvjs_build_data_attributes(array $options): Attribute {
$list_attributes = [];
foreach (VvjsConstants::DATA_ATTRIBUTE_MAP as $option_key => $data_key) {
if (!empty($options[$option_key])) {
$list_attributes[VvjsConstants::DATA_ATTRIBUTE_PREFIX . $data_key] = $options[$option_key];
}
}
foreach (VvjsConstants::BOOLEAN_ATTRIBUTE_MAP as $option_key => $data_key) {
if (isset($options[$option_key])) {
$list_attributes[VvjsConstants::DATA_ATTRIBUTE_PREFIX . $data_key] = $options[$option_key] ? 'true' : 'false';
}
}
return new Attribute($list_attributes);
}
/**
* Calculate background RGB value with opacity.
*
* @param array $options
* The slideshow configuration options.
*
* @return string|null
* RGBA color string or NULL if no overlay color configured.
*/
function _vvjs_calculate_background_rgb(array $options): ?string {
if (empty($options['overlay_bg_color'])) {
return NULL;
}
try {
$rgb = _vvjs_hex_to_rgb($options['overlay_bg_color']);
$opacity = $options['overlay_bg_opacity'] ?? VvjsConstants::DEFAULT_OPACITY;
$opacity = max(0, min(1, (float) $opacity));
return "rgba({$rgb['r']}, {$rgb['g']}, {$rgb['b']}, $opacity)";
}
catch (\InvalidArgumentException $e) {
\Drupal::logger('vvjs')->warning('Invalid overlay background color: @color', [
'@color' => $options['overlay_bg_color'],
]);
return NULL;
}
}
/**
* Build template settings array.
*
* @param object $handler
* The style plugin handler.
* @param array $options
* The slideshow configuration options.
*
* @return array
* Settings array for template use.
*/
function _vvjs_build_template_settings(object $handler, array $options): array {
$view_id = $handler->view->dom_id ?? 'unknown';
return [
'view_id' => $view_id,
'animation' => $options['animation'] ?? '',
'time_in_seconds' => $options['time_in_seconds'] ?? 0,
'navigation' => $options['navigation'] ?? '',
'arrows' => $options['arrows'] ?? '',
'unique_id' => $options['unique_id'] ?? 0,
'available_breakpoints' => $options['available_breakpoints'] ?? '',
'enable_css' => $options['enable_css'] ?? TRUE,
'min_height' => $options['min_height'] ?? 0,
'max_content_width' => $options['max_content_width'] ?? 0,
'max_width' => $options['max_width'] ?? 0,
'hero_slideshow' => $options['hero_slideshow'] ?? FALSE,
'enable_deeplink' => $options['enable_deeplink'] ?? FALSE,
'deeplink_identifier' => $options['deeplink_identifier'] ?? '',
'transition_type' => $options['transition_type'] ?? VvjsConstants::TRANSITION_INSTANT,
'transition_duration' => $options['transition_duration'] ?? VvjsConstants::TRANSITION_DURATION_DEFAULT,
];
}
/**
* Customize theme hook suggestions for slideshow rows.
*
* @param array $variables
* Template variables array (passed by reference).
*/
function _vvjs_customize_row_theme_suggestions(array &$variables): void {
if (empty($variables['rows'])) {
return;
}
foreach ($variables['rows'] as $key => $row) {
if (!isset($row['#theme']) || !is_array($row['#theme'])) {
continue;
}
foreach ($row['#theme'] as $idx => $theme_hook_suggestion) {
$variables['rows'][$key]['#theme'][$idx] = str_replace(
'views_view_fields',
'views_view_vvjs_fields',
$theme_hook_suggestion
);
}
}
}
/**
* Prepares variables for views_view_vvjs_fields template.
*
* @param array $variables
* Template variables array (passed by reference).
*/
function template_preprocess_views_view_vvjs_fields(array &$variables): void {
\Drupal::moduleHandler()->loadInclude('views', 'inc', 'views.theme');
template_preprocess_views_view_fields($variables);
}
/**
* Implements hook_preprocess_views_view().
*/
function vvjs_preprocess_views_view(array &$variables): void {
if (isset($variables['view']->style_plugin) &&
$variables['view']->style_plugin instanceof ViewsVanillaJavascriptSlideshow) {
$variables['attributes']['class'][] = 'vvj-slideshow';
}
}
/**
* Helper function to convert hex color to RGB with enhanced validation.
*
* @param string $hex
* The hex color code (with or without # prefix).
*
* @return array
* An associative array with 'r', 'g', 'b' integer values.
*
* @throws \InvalidArgumentException
* When the hex color is invalid.
*/
function _vvjs_hex_to_rgb(string $hex): array {
$hex = trim($hex);
if (empty($hex)) {
throw new \InvalidArgumentException('Hex color cannot be empty');
}
$hex = ltrim($hex, '#');
if (!_vvjs_validate_hex_color($hex)) {
throw new \InvalidArgumentException("Invalid hex color format: #{$hex}");
}
if (strlen($hex) === 3) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
return [
'r' => hexdec(substr($hex, 0, 2)),
'g' => hexdec(substr($hex, 2, 2)),
'b' => hexdec(substr($hex, 4, 2)),
];
}
/**
* Validate hex color format.
*
* @param string $hex
* Hex color string without # prefix.
*
* @return bool
* TRUE if valid hex color, FALSE otherwise.
*/
function _vvjs_validate_hex_color(string $hex): bool {
$length = strlen($hex);
if ($length !== 3 && $length !== 6) {
return FALSE;
}
return ctype_xdigit($hex);
}
/**
* Implements hook_views_data_alter().
*/
function vvjs_views_data_alter(array &$data): void {
$data['views_style_plugin']['views_vvjs'] = [
'type' => 'views_style',
'label' => t('Views Vanilla JavaScript Slideshow'),
'mapping' => [
'time_in_seconds' => [
'type' => VvjsConstants::VIEWS_TYPE_INTEGER,
'label' => t('Auto-advance Time (milliseconds)'),
'description' => t('Time between automatic slide transitions. Set to 0 to disable auto-advance.'),
'constraints' => [
'Range' => [
'min' => VvjsConstants::VIEWS_MIN_TIME,
'max' => VvjsConstants::VIEWS_MAX_TIME,
],
],
],
'navigation' => [
'type' => VvjsConstants::VIEWS_TYPE_STRING,
'label' => t('Slide Indicators Type'),
'description' => t('Type of navigation indicators to display at the bottom of the slideshow.'),
'constraints' => [
'Choice' => ['none', 'dots', 'numbers'],
],
],
'arrows' => [
'type' => VvjsConstants::VIEWS_TYPE_STRING,
'label' => t('Navigation Arrows Position'),
'description' => t('Position and visibility of navigation arrows for manual slide control.'),
'constraints' => [
'Choice' => [
'none',
'arrows-sides',
'arrows-sides-big',
'arrows-top',
'arrows-top-big',
],
],
],
'animation' => [
'type' => VvjsConstants::VIEWS_TYPE_STRING,
'label' => t('Slide Transition Animation'),
'description' => t('Type of animation effect used when transitioning between slides.'),
'constraints' => [
'Choice' => [
'none',
'a-zoom',
'a-fade',
'a-top',
'a-bottom',
'a-left',
'a-right',
],
],
],
'hero_slideshow' => [
'type' => VvjsConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Enable Hero Slideshow Mode'),
'description' => t('Enable full-width hero slideshow with overlay content positioning.'),
],
'overlay_bg_color' => [
'type' => VvjsConstants::VIEWS_TYPE_STRING,
'label' => t('Overlay Background Color'),
'description' => t('Hex color code for the content overlay background (e.g., #000000).'),
'constraints' => [
'Regex' => [
'pattern' => '/^#[0-9A-Fa-f]{6}$/',
'message' => t('Must be a valid 6-digit hex color code (e.g., #000000).'),
],
],
],
'overlay_bg_opacity' => [
'type' => VvjsConstants::VIEWS_TYPE_FLOAT,
'label' => t('Overlay Background Opacity'),
'description' => t('Opacity level for the overlay background, from 0 (transparent) to 1 (opaque).'),
'constraints' => [
'Range' => [
'min' => VvjsConstants::VIEWS_MIN_OPACITY,
'max' => VvjsConstants::VIEWS_MAX_OPACITY,
],
],
],
'overlay_position' => [
'type' => VvjsConstants::VIEWS_TYPE_STRING,
'label' => t('Overlay Content Position'),
'description' => t('Position of the content overlay within the hero slideshow area.'),
'constraints' => [
'Choice' => [
'd-full',
'd-middle',
'd-left',
'd-right',
'd-top',
'd-bottom',
'd-top-left',
'd-top-right',
'd-bottom-left',
'd-bottom-right',
'd-top-middle',
'd-bottom-middle',
],
],
],
'available_breakpoints' => [
'type' => VvjsConstants::VIEWS_TYPE_STRING,
'label' => t('Responsive Breakpoint'),
'description' => t('Maximum screen width at which hero slideshow features are enabled.'),
'constraints' => [
'Choice' => ['576', '768', '992', '1200', '1400'],
],
],
'min_height' => [
'type' => VvjsConstants::VIEWS_TYPE_INTEGER,
'label' => t('Minimum Height (viewport width units)'),
'description' => t('Minimum height for hero slideshow in vw units (1-200).'),
'constraints' => [
'Range' => [
'min' => VvjsConstants::VIEWS_MIN_HEIGHT,
'max' => VvjsConstants::VIEWS_MAX_HEIGHT,
],
],
],
'max_content_width' => [
'type' => VvjsConstants::VIEWS_TYPE_INTEGER,
'label' => t('Maximum Content Width (percentage)'),
'description' => t('Maximum width for overlay content as percentage of container (1-100).'),
'constraints' => [
'Range' => [
'min' => VvjsConstants::VIEWS_MIN_CONTENT_WIDTH,
'max' => VvjsConstants::VIEWS_MAX_CONTENT_WIDTH,
],
],
],
'max_width' => [
'type' => VvjsConstants::VIEWS_TYPE_INTEGER,
'label' => t('Maximum Container Width (pixels)'),
'description' => t('Maximum width for the slideshow container in pixels (1-9999).'),
'constraints' => [
'Range' => [
'min' => VvjsConstants::VIEWS_MIN_WIDTH,
'max' => VvjsConstants::VIEWS_MAX_WIDTH,
],
],
],
'show_total_slides' => [
'type' => VvjsConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Show Total Slide Count'),
'description' => t('Display current slide number and total count (e.g., "1 of 5").'),
],
'show_play_pause' => [
'type' => VvjsConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Show Play/Pause Button'),
'description' => t('Display play/pause button for manual control of auto-advance.'),
],
'show_slide_progress' => [
'type' => VvjsConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Show Slide Progress Indicator'),
'description' => t('Display animated progress indicator showing slide timing progress.'),
],
'unique_id' => [
'type' => VvjsConstants::VIEWS_TYPE_INTEGER,
'label' => t('Unique Slideshow Identifier'),
'description' => t('Unique numeric identifier for this slideshow instance (auto-generated).'),
'constraints' => [
'Range' => [
'min' => 10000000,
'max' => 99999999,
],
],
],
'enable_css' => [
'type' => VvjsConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Enable Default CSS Styling'),
'description' => t('Include the default CSS library for slideshow styling and layout.'),
],
'transition_type' => [
'type' => VvjsConstants::VIEWS_TYPE_STRING,
'label' => t('Transition Type'),
'description' => t('The type of transition effect between slides.'),
'constraints' => [
'Choice' => [
VvjsConstants::TRANSITION_INSTANT,
VvjsConstants::TRANSITION_CROSSFADE_CLASSIC,
VvjsConstants::TRANSITION_CROSSFADE_STAGED,
VvjsConstants::TRANSITION_CROSSFADE_DYNAMIC,
],
],
],
'transition_duration' => [
'type' => VvjsConstants::VIEWS_TYPE_INTEGER,
'label' => t('Transition Duration (milliseconds)'),
'description' => t('Duration of crossfade transition in milliseconds.'),
'constraints' => [
'Range' => [
'min' => VvjsConstants::TRANSITION_DURATION_MIN,
'max' => VvjsConstants::TRANSITION_DURATION_MAX,
],
],
],
],
];
}
/**
* Implements hook_token_info().
*/
function vvjs_token_info(): array {
return [
'tokens' => [
'view' => [
VvjsConstants::TOKEN_NAMESPACE => [
'name' => t('VVJS field output'),
'description' => t("Use these tokens when you enable 'Use replacement tokens from the first row' in Views text areas such as the header, footer, or empty text. Use [vvjs:field_name] for rendered output, or [vvjs:field_name:plain] to strip HTML and return plain text. These tokens pull values from the first row of the View result."),
],
],
],
];
}
/**
* Implements hook_tokens().
*/
function vvjs_tokens(string $type, array $tokens, array $data = [], array $options = []): array {
$replacements = [];
if (!_vvjs_validate_token_context($type, $data)) {
return $replacements;
}
$view = $data['view'];
if (empty($view->result)) {
return $replacements;
}
if (!($view->style_plugin instanceof ViewsVanillaJavascriptSlideshow)) {
return $replacements;
}
try {
$first_row = $view->result[0];
$field_handlers = $view->display_handler->getHandlers('field');
$renderer = \Drupal::service('renderer');
foreach ($tokens as $token => $name) {
if (!preg_match(VvjsConstants::TOKEN_PATTERN, $token)) {
\Drupal::logger('vvjs')->warning('Invalid token format rejected: @token', [
'@token' => $token,
]);
continue;
}
$replacement = _vvjs_process_single_token($token, $first_row, $field_handlers, $renderer);
if ($replacement !== NULL) {
$replacements["[" . VvjsConstants::TOKEN_NAMESPACE . ":$token]"] = $replacement;
}
}
}
catch (\Exception $e) {
\Drupal::logger('vvjs')->error('Error processing VVJS tokens: @message', [
'@message' => $e->getMessage(),
]);
}
return $replacements;
}
/**
* Validate token processing context.
*
* @param string $type
* Token type.
* @param array $data
* Token data array.
*
* @return bool
* TRUE if context is valid for processing.
*/
function _vvjs_validate_token_context(string $type, array $data): bool {
return $type === 'view' &&
isset($data['view']) &&
$data['view'] instanceof ViewExecutable;
}
/**
* Process a single token and return its replacement value.
*
* @param string $token
* The token to process.
* @param object $first_row
* The first row of view results.
* @param array $field_handlers
* Array of field handlers.
* @param object $renderer
* The renderer service.
*
* @return \Drupal\Component\Render\MarkupInterface|string|null
* The replacement value or NULL if token cannot be processed.
*/
function _vvjs_process_single_token(string $token, object $first_row, array $field_handlers, object $renderer) {
$plain = FALSE;
$field_id = $token;
if (str_ends_with($token, VvjsConstants::TOKEN_PLAIN_SUFFIX)) {
$plain = TRUE;
$field_id = substr($token, 0, -strlen(VvjsConstants::TOKEN_PLAIN_SUFFIX));
}
if (!isset($field_handlers[$field_id])) {
return NULL;
}
try {
$handler = $field_handlers[$field_id];
$value = $plain && method_exists($handler, 'advancedRenderText')
? $handler->advancedRenderText($first_row)
: $handler->advancedRender($first_row);
if (is_array($value)) {
$rendered = $renderer->renderPlain($value);
}
else {
$rendered = (string) $value;
}
return $plain
? Html::decodeEntities(strip_tags($rendered))
: Markup::create($rendered);
}
catch (\Throwable $e) {
\Drupal::logger('vvjs')->warning('Token processing failed for field @field: @message', [
'@field' => $field_id,
'@message' => $e->getMessage(),
]);
return '';
}
}
