vvjp-1.0.3/vvjp.module
vvjp.module
<?php
/**
* @file
* Provides the module implementation for vvjp.
*
* Contains template preprocessing and theme definitions for Views.
*
* Filename: vvjp.module
* Website: https://www.flashwebcenter.com
* Description: Views Vanilla JavaScript Parallax module.
* Developer: Alaa Haddad https://www.alaahaddad.com.
*/
declare(strict_types=1);
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Markup;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Template\Attribute;
use Drupal\views\ViewExecutable;
use Drupal\vvjp\Plugin\views\style\ViewsVanillaJavascriptParallax;
use Drupal\vvjp\VvjpConstants;
/**
* Implements hook_help().
*/
function vvjp_help(string $route_name, RouteMatchInterface $route_match): ?string {
if ($route_name === 'help.page.vvjp') {
return _vvjp_helper_render_readme();
}
return NULL;
}
/**
* Helper function to render README.md.
*
* @return string
* The rendered content of README.md.
*/
function _vvjp_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 vvjp_theme(array $existing, string $type, string $theme, string $path): array {
return [
VvjpConstants::THEME_HOOK_FIELDS => [
'variables' => [
'view' => NULL,
'options' => [],
'row' => NULL,
'field_alias' => NULL,
'attributes' => [],
'title_attributes' => [],
'content_attributes' => [],
'title_prefix' => [],
'title_suffix' => [],
'fields' => [],
],
'template' => 'views-view-vvjp-fields',
'path' => $path . '/templates',
],
VvjpConstants::THEME_HOOK => [
'variables' => [
'view' => NULL,
'rows' => [],
'options' => [],
],
'template' => 'views-view-vvjp',
'path' => $path . '/templates',
],
];
}
/**
* Implements hook_preprocess_HOOK() for views_view_vvjp.
*/
function template_preprocess_views_view_vvjp(array &$variables): void {
static $views_theme_loaded = FALSE;
if (!$views_theme_loaded) {
\Drupal::moduleHandler()->loadInclude('views', 'inc', 'views.theme');
$views_theme_loaded = TRUE;
}
$handler = $variables['view']->style_plugin;
$list_attributes = [];
// Add parallax-specific options as data attributes.
if (!empty($handler->options[VvjpConstants::OPTION_PARALLAX_SPEED])) {
$list_attributes[VvjpConstants::DATA_ATTR_PARALLAX_SPEED] = $handler->options[VvjpConstants::OPTION_PARALLAX_SPEED];
}
if (!empty($handler->options[VvjpConstants::OPTION_BG_POSITION])) {
$list_attributes[VvjpConstants::DATA_ATTR_BG_POSITION] = $handler->options[VvjpConstants::OPTION_BG_POSITION];
}
if (!empty($handler->options[VvjpConstants::OPTION_BG_SPEED])) {
$list_attributes[VvjpConstants::DATA_ATTR_BG_SPEED] = $handler->options[VvjpConstants::OPTION_BG_SPEED];
}
if (!empty($handler->options[VvjpConstants::OPTION_SCROLL_EFFECT])) {
$list_attributes[VvjpConstants::DATA_ATTR_SCROLL_EFFECT] = $handler->options[VvjpConstants::OPTION_SCROLL_EFFECT];
}
if (!empty($handler->options[VvjpConstants::OPTION_DISABLE_OVERLAY])) {
$list_attributes[VvjpConstants::DATA_ATTR_DISABLE_OVERLAY] = $handler->options[VvjpConstants::OPTION_DISABLE_OVERLAY] ? 'true' : 'false';
}
if (!empty($handler->options[VvjpConstants::OPTION_OVER_CONTENT_ONLY])) {
$list_attributes[VvjpConstants::DATA_ATTR_OVER_CONTENT_ONLY] = $handler->options[VvjpConstants::OPTION_OVER_CONTENT_ONLY] ? 'true' : 'false';
}
if (!empty($handler->options[VvjpConstants::OPTION_MAX_WIDTH])) {
$list_attributes[VvjpConstants::DATA_ATTR_MAX_WIDTH] = $handler->options[VvjpConstants::OPTION_MAX_WIDTH];
}
// Calculate and pass the background_rgb variable to the template.
if (!empty($handler->options[VvjpConstants::OPTION_OVERLAY_COLOR])) {
$rgb = _vvjp_hex_to_rgb($handler->options[VvjpConstants::OPTION_OVERLAY_COLOR]);
$opacity = $handler->options[VvjpConstants::OPTION_OVERLAY_OPACITY] ?? VvjpConstants::MAX_OVERLAY_OPACITY;
$variables['background_rgb'] = sprintf(
'rgba(%d, %d, %d, %s)',
$rgb['r'],
$rgb['g'],
$rgb['b'],
$opacity
);
}
else {
$variables['background_rgb'] = NULL;
}
// Process the section height as a combined value.
$section_height = VvjpConstants::DEFAULT_HEIGHT_VALUE . VvjpConstants::DEFAULT_HEIGHT_UNIT;
if (!empty($handler->options[VvjpConstants::OPTION_SECTION_HEIGHT]['value']) &&
!empty($handler->options[VvjpConstants::OPTION_SECTION_HEIGHT]['unit'])) {
$section_height = $handler->options[VvjpConstants::OPTION_SECTION_HEIGHT]['value'] .
$handler->options[VvjpConstants::OPTION_SECTION_HEIGHT]['unit'];
}
$variables['section_height'] = $section_height;
if (!empty($handler->options[VvjpConstants::OPTION_BREAKPOINTS])) {
$list_attributes[VvjpConstants::DATA_ATTR_BREAKPOINTS] = $handler->options[VvjpConstants::OPTION_BREAKPOINTS];
}
if (!empty($handler->options[VvjpConstants::OPTION_BG_EASING])) {
$list_attributes[VvjpConstants::DATA_ATTR_EASING] = $handler->options[VvjpConstants::OPTION_BG_EASING];
}
if (!empty($handler->options[VvjpConstants::OPTION_ENABLE_CSS])) {
$list_attributes[VvjpConstants::DATA_ATTR_ENABLE_CSS] = $handler->options[VvjpConstants::OPTION_ENABLE_CSS] ? 'true' : 'false';
}
$variables['list_attributes'] = new Attribute($list_attributes);
$variables['options'] = $handler->options;
// Pass additional settings to the template.
$variables['settings'] = [
'view_id' => $variables['view']->dom_id,
VvjpConstants::OPTION_PARALLAX_SPEED => $handler->options[VvjpConstants::OPTION_PARALLAX_SPEED],
VvjpConstants::OPTION_BG_POSITION => $handler->options[VvjpConstants::OPTION_BG_POSITION],
VvjpConstants::OPTION_BG_SPEED => $handler->options[VvjpConstants::OPTION_BG_SPEED],
VvjpConstants::OPTION_SCROLL_EFFECT => $handler->options[VvjpConstants::OPTION_SCROLL_EFFECT],
VvjpConstants::OPTION_OVERLAY_COLOR => $handler->options[VvjpConstants::OPTION_OVERLAY_COLOR],
VvjpConstants::OPTION_OVERLAY_OPACITY => $handler->options[VvjpConstants::OPTION_OVERLAY_OPACITY],
VvjpConstants::OPTION_SECTION_HEIGHT => $section_height,
VvjpConstants::OPTION_BREAKPOINTS => $handler->options[VvjpConstants::OPTION_BREAKPOINTS],
VvjpConstants::OPTION_BG_EASING => $handler->options[VvjpConstants::OPTION_BG_EASING],
VvjpConstants::OPTION_ENABLE_CSS => $handler->options[VvjpConstants::OPTION_ENABLE_CSS],
VvjpConstants::OPTION_OVER_CONTENT_ONLY => $handler->options[VvjpConstants::OPTION_OVER_CONTENT_ONLY],
VvjpConstants::OPTION_MAX_WIDTH => $handler->options[VvjpConstants::OPTION_MAX_WIDTH],
];
// Customize theme hook suggestions for rows.
if (!empty($variables['rows'])) {
foreach ($variables['rows'] as $key => $row) {
if (isset($row['#theme']) && is_array($row['#theme'])) {
foreach ($row['#theme'] as $idx => $theme_hook_suggestion) {
$variables['rows'][$key]['#theme'][$idx] = str_replace(
'views_view_fields',
VvjpConstants::THEME_HOOK_FIELDS,
$theme_hook_suggestion
);
}
}
}
}
template_preprocess_views_view_unformatted($variables);
}
/**
* Prepares variables for views_view_vvjp_fields template.
*
* @param array $variables
* An associative array containing:
* - view: The view object.
* - options: Configuration options.
* - row: The result row object.
* - fields: An array of field render arrays.
*/
function template_preprocess_views_view_vvjp_fields(array &$variables): void {
static $views_theme_loaded = FALSE;
if (!$views_theme_loaded) {
\Drupal::moduleHandler()->loadInclude('views', 'inc', 'views.theme');
$views_theme_loaded = TRUE;
}
template_preprocess_views_view_fields($variables);
}
/**
* Implements hook_preprocess_views_view().
*/
function vvjp_preprocess_views_view(array &$variables): void {
if ($variables['view']->style_plugin instanceof ViewsVanillaJavascriptParallax) {
$variables['attributes']['class'][] = VvjpConstants::CSS_CLASS_WRAPPER;
}
}
/**
* Helper function to convert hex color to RGB.
*
* @param string $hex
* The hex color code.
*
* @return array
* An associative array with 'r', 'g', 'b' values.
*/
function _vvjp_hex_to_rgb(string $hex): array {
$hex = ltrim($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)),
];
}
/**
* Implements hook_views_data_alter().
*/
function vvjp_views_data_alter(array &$data): void {
$data['views_style_plugin'][VvjpConstants::PLUGIN_ID] = [
'type' => 'views_style',
'label' => t('Views Vanilla JavaScript Parallax'),
'mapping' => [
VvjpConstants::OPTION_UNIQUE_ID => [
'type' => 'integer',
'label' => t('Unique ID'),
'description' => t('Unique identifier for the view display.'),
],
VvjpConstants::OPTION_PARALLAX_SPEED => [
'type' => 'numeric',
'label' => t('Parallax Speed'),
'description' => t('Set the speed of the parallax effect.'),
],
VvjpConstants::OPTION_BG_POSITION => [
'type' => 'string',
'label' => t('Background Position'),
'description' => t('Position of the background image.'),
],
VvjpConstants::OPTION_OVERLAY_COLOR => [
'type' => 'string',
'label' => t('Overlay Color'),
'description' => t('Select the overlay color for the parallax background.'),
],
VvjpConstants::OPTION_OVERLAY_OPACITY => [
'type' => 'float',
'label' => t('Overlay Opacity'),
'description' => t('Set the opacity level for the overlay.'),
],
VvjpConstants::OPTION_SECTION_HEIGHT => [
'type' => 'array',
'label' => t('Section Height'),
'description' => t('Define the height of the parallax section.'),
],
VvjpConstants::OPTION_MAX_WIDTH => [
'type' => 'numeric',
'label' => t('Max Width for Parallax content'),
'description' => t('The max width for the parallax content.'),
],
VvjpConstants::OPTION_BREAKPOINTS => [
'type' => 'string',
'label' => t('Responsive Breakpoints'),
'description' => t('Define breakpoints for responsive design.'),
],
VvjpConstants::OPTION_BG_EASING => [
'type' => 'string',
'label' => t('Background Animation Easing'),
'description' => t('Choose the easing function for animations.'),
],
VvjpConstants::OPTION_BG_SPEED => [
'type' => 'numeric',
'label' => t('Background Animation Speed'),
'description' => t('Set the speed of the background animation.'),
],
VvjpConstants::OPTION_SCROLL_EFFECT => [
'type' => 'string',
'label' => t('Scroll Effect'),
'description' => t('Choose the scroll effect for the parallax content.'),
],
VvjpConstants::OPTION_ENABLE_CSS => [
'type' => 'boolean',
'label' => t('Enable CSS Library'),
'description' => t('Enable or disable the CSS library for styling the parallax.'),
],
VvjpConstants::OPTION_DISABLE_OVERLAY => [
'type' => 'boolean',
'label' => t('Disable Overlay Color'),
'description' => t('Enable or disable the overlay color completely.'),
],
VvjpConstants::OPTION_OVER_CONTENT_ONLY => [
'type' => 'boolean',
'label' => t('Apply Background Color for Content Only'),
'description' => t('Enable this option to apply the overlay background color only beneath the parallax content, rather than covering the entire row.'),
],
],
];
}
/**
* Implements hook_token_info().
*/
function vvjp_token_info(): array {
return [
'tokens' => [
'view' => [
VvjpConstants::TOKEN_NAMESPACE => [
'name' => t('VVJP 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 [vvjp:field_name] for rendered output, or [vvjp: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 vvjp_tokens(string $type, array $tokens, array $data = [], array $options = []): array {
$replacements = [];
if (!in_array($type, [VvjpConstants::TOKEN_NAMESPACE, 'global'])) {
return $replacements;
}
if (!isset($data['view']) || !($data['view'] instanceof ViewExecutable)) {
return $replacements;
}
$view = $data['view'];
if (!($view->style_plugin instanceof ViewsVanillaJavascriptParallax)) {
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(VvjpConstants::TOKEN_PATTERN, $token)) {
\Drupal::logger('vvjp')->warning('Invalid token format: @token', ['@token' => $token]);
continue;
}
$plain = FALSE;
$field_id = $token;
if (str_ends_with($token, VvjpConstants::TOKEN_SUFFIX_PLAIN)) {
$plain = TRUE;
$field_id = substr($token, 0, -strlen(VvjpConstants::TOKEN_SUFFIX_PLAIN));
}
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["[" . VvjpConstants::TOKEN_NAMESPACE . ":$token]"] = $plain
? Html::decodeEntities(strip_tags($rendered))
: Markup::create($rendered);
}
catch (\Throwable $e) {
\Drupal::logger('vvjp')->error('Token replacement failed for @token: @message', [
'@token' => $token,
'@message' => $e->getMessage(),
]);
$replacements["[" . VvjpConstants::TOKEN_NAMESPACE . ":$token]"] = '';
}
}
return $replacements;
}
