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 '';
  }
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc