vvjb-1.0.x-dev/vvjb.module
vvjb.module
<?php
/**
* @file
* Provides the module implementation for vvjb.
*
* Contains template preprocessing and theme definitions for Views.
*
* Filename: vvjb.module
* Website: https://www.flashwebcenter.com
* Description: Basic JS carousel Views plugin.
* 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\vvjb\Plugin\views\style\ViewsVanillaJavascriptBasicCarousel;
use Drupal\vvjb\VvjbConstants;
/**
* Implements hook_help().
*/
function vvjb_help(string $route_name, RouteMatchInterface $route_match): ?string {
if ($route_name === 'help.page.vvjb') {
return _vvjb_helper_render_readme();
}
return NULL;
}
/**
* Helper function to render README.md.
*
* @return string
* The rendered content of README.md.
*/
function _vvjb_helper_render_readme(): string {
$readme_path = __DIR__ . '/README.md';
$text = file_get_contents($readme_path);
if ($text === FALSE) {
return (string) t('README.md file not found.');
}
if (!\Drupal::moduleHandler()->moduleExists('markdown')) {
return '<pre>' . htmlspecialchars($text) . '</pre>';
}
$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();
}
/**
* Implements hook_theme().
*/
function vvjb_theme(array $existing, string $type, string $theme, string $path): array {
return [
'views_view_vvjb' => [
'variables' => [
'view' => NULL,
'rows' => [],
'options' => [],
],
'template' => 'views-view-vvjb',
'path' => $path . '/templates',
],
];
}
/**
* Implements hook_preprocess_HOOK() for views_view_vvjb.
*/
function template_preprocess_views_view_vvjb(array &$variables): void {
static $views_theme_loaded = FALSE;
if (!$views_theme_loaded) {
\Drupal::moduleHandler()->loadInclude('views', 'inc', 'views.theme');
$views_theme_loaded = TRUE;
}
/** @var \Drupal\vvjb\Plugin\views\style\ViewsVanillaJavascriptBasicCarousel $handler */
$handler = $variables['view']->style_plugin;
$list_attributes = [];
foreach (VvjbConstants::DATA_ATTRIBUTE_MAP as $option_key => $data_key) {
if (isset($handler->options[$option_key])) {
$value = $handler->options[$option_key];
if (is_bool($value)) {
$value = $value ? 'true' : 'false';
}
elseif ($option_key === 'looping') {
$value = $value ? '1' : '0';
}
$list_attributes['data-' . $data_key] = (string) $value;
}
}
// Add deep linking configuration.
$variables['deeplink_enabled'] = $handler->options['enable_deeplink'] ?? FALSE;
$variables['deeplink_identifier'] = $handler->options['deeplink_identifier'] ?? '';
$variables['list_attributes'] = new Attribute($list_attributes);
$variables['settings'] = $handler->options;
template_preprocess_views_view_unformatted($variables);
}
/**
* Implements hook_preprocess_views_view().
*/
function vvjb_preprocess_views_view(array &$variables): void {
if ($variables['view']->style_plugin instanceof ViewsVanillaJavascriptBasicCarousel) {
$variables['attributes']['class'][] = 'vvj-basic-carousel';
}
}
/**
* Implements hook_token_info().
*/
function vvjb_token_info(): array {
return [
'tokens' => [
'view' => [
'vvjb' => [
'name' => t('VVJB 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 [vvjb:field_name] for rendered output, or [vvjb: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 vvjb_tokens(string $type, array $tokens, array $data = [], array $options = []): array {
$replacements = [];
if (!in_array($type, ['vvjb', 'global'])) {
return $replacements;
}
if (!isset($data['view']) || !($data['view'] instanceof ViewExecutable)) {
return $replacements;
}
$view = $data['view'];
if (!($view->style_plugin instanceof ViewsVanillaJavascriptBasicCarousel)) {
return $replacements;
}
if (empty($view->result)) {
return $replacements;
}
$first_row = $view->result[0];
$field_handlers = $view->display_handler->getHandlers('field');
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
foreach ($tokens as $token => $name) {
if (!preg_match(VvjbConstants::TOKEN_PATTERN, $token)) {
\Drupal::logger('vvjb')->warning('Invalid token format: @token', ['@token' => $token]);
continue;
}
$plain = FALSE;
$field_id = $token;
if (str_ends_with($token, VvjbConstants::TOKEN_PLAIN_SUFFIX)) {
$plain = TRUE;
$field_id = substr($token, 0, -strlen(VvjbConstants::TOKEN_PLAIN_SUFFIX));
}
if (!isset($field_handlers[$field_id])) {
continue;
}
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;
}
$replacements['[' . VvjbConstants::TOKEN_NAMESPACE . ':' . $token . ']'] = $plain
? Html::decodeEntities(strip_tags($rendered))
: Markup::create($rendered);
}
catch (\Throwable $e) {
\Drupal::logger('vvjb')->error('Token replacement failed for @token: @message', [
'@token' => $token,
'@message' => $e->getMessage(),
]);
$replacements['[' . VvjbConstants::TOKEN_NAMESPACE . ':' . $token . ']'] = '';
}
}
return $replacements;
}
/**
* Implements hook_views_data_alter().
*/
function vvjb_views_data_alter(array &$data): void {
$data['views_style_plugin']['views_vvjb'] = [
'type' => 'views_style',
'label' => t('Basic Vanilla JavaScript Carousel'),
'mapping' => [
'unique_id' => [
'type' => VvjbConstants::VIEWS_TYPE_STRING,
'label' => t('Unique ID for the view display'),
],
'orientation' => [
'type' => VvjbConstants::VIEWS_TYPE_STRING,
'label' => t('Carousel Orientation'),
],
'items_small' => [
'type' => VvjbConstants::VIEWS_TYPE_INTEGER,
'label' => t('Items per screen (small)'),
],
'items_big' => [
'type' => VvjbConstants::VIEWS_TYPE_INTEGER,
'label' => t('Items per screen (large)'),
],
'gap' => [
'type' => VvjbConstants::VIEWS_TYPE_INTEGER,
'label' => t('Gap between items (px)'),
],
'item_width' => [
'type' => VvjbConstants::VIEWS_TYPE_INTEGER,
'label' => t('Custom item width (px)'),
],
'looping' => [
'type' => VvjbConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Enable looping'),
],
'slide_time' => [
'type' => VvjbConstants::VIEWS_TYPE_INTEGER,
'label' => t('Autoplay interval (ms)'),
],
'navigation' => [
'type' => VvjbConstants::VIEWS_TYPE_STRING,
'label' => t('Navigation type'),
],
'breakpoints' => [
'type' => VvjbConstants::VIEWS_TYPE_STRING,
'label' => t('Responsive Breakpoint'),
],
'show_play_pause' => [
'type' => VvjbConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Show Play/Pause Button'),
'description' => t('Display a button to control carousel auto-rotation.'),
],
'show_page_counter' => [
'type' => VvjbConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Show Page Counter'),
'description' => t('Display current page number.'),
],
'show_progress_bar' => [
'type' => VvjbConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Show Progress Bar'),
'description' => t('Display a visual progress indicator showing time until next page.'),
],
'enable_keyboard_nav' => [
'type' => VvjbConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Enable Enhanced Keyboard Navigation'),
'description' => t('Enable keyboard controls for carousel navigation.'),
],
'enable_touch_swipe' => [
'type' => VvjbConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Enable Touch/Swipe Gestures'),
'description' => t('Allow users to navigate the carousel by swiping on touch devices.'),
],
'enable_pause_on_hover' => [
'type' => VvjbConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Pause on Hover'),
'description' => t('Pause the carousel rotation when the user hovers over it.'),
],
'pause_on_reduced_motion' => [
'type' => VvjbConstants::VIEWS_TYPE_BOOLEAN,
'label' => t('Respect Reduced Motion Preference'),
'description' => t('Automatically pause carousel rotation for users who prefer reduced motion.'),
],
],
];
}
