Files
roi-theme/Public/Navbar/Infrastructure/Ui/NavbarRenderer.php
FrankZamora 2831cabec9 Fix: ROI_Bootstrap_Nav_Walker - allow dropdown links with URLs to navigate
- Apply same fix to NavbarRenderer's walker class
- Only add data-bs-toggle=dropdown for items without real URL
- Fixes Buscador General link navigation
2025-11-26 23:49:10 -06:00

370 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace ROITheme\Public\Navbar\Infrastructure\Ui;
use ROITheme\Shared\Domain\Entities\Component;
use ROITheme\Shared\Domain\Contracts\RendererInterface;
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
use Walker_Nav_Menu;
/**
* NavbarRenderer - Renderiza el menú de navegación principal
*
* RESPONSABILIDAD: Generar HTML del menú de navegación WordPress
*
* CARACTERÍSTICAS:
* - Integración con wp_nav_menu()
* - Walker personalizado para Bootstrap 5
* - Soporte para submenús desplegables
* - Responsive con navbar-toggler
*
* Cumple con:
* - DIP: Recibe CSSGeneratorInterface por constructor
* - SRP: Una responsabilidad (renderizar navbar)
* - Clean Architecture: Infrastructure puede usar WordPress
*
* @package ROITheme\Public\Navbar\Infrastructure\Ui
*/
final class NavbarRenderer implements RendererInterface
{
/**
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
*/
public function __construct(
private CSSGeneratorInterface $cssGenerator
) {}
public function render(Component $component): string
{
$data = $component->getData();
if (!$this->isEnabled($data)) {
return '';
}
$css = $this->generateCSS($data);
$html = $this->buildMenu($data);
return sprintf(
"<style>%s</style>\n%s",
$css,
$html
);
}
private function isEnabled(array $data): bool
{
return isset($data['visibility']['is_enabled']) &&
$data['visibility']['is_enabled'] === true;
}
private function shouldShowOnMobile(array $data): bool
{
return isset($data['visibility']['show_on_mobile']) &&
$data['visibility']['show_on_mobile'] === true;
}
/**
* Generar CSS usando CSSGeneratorService
*
* @param array $data Datos del componente
* @return string CSS generado
*/
private function generateCSS(array $data): string
{
$css = '';
// Obtener valores de configuración
$stickyEnabled = $data['visibility']['sticky_enabled'] ?? true;
$paddingVertical = $data['layout']['padding_vertical'] ?? '0.75rem 0';
$zIndex = $data['layout']['z_index'] ?? '1030';
$bgColor = $data['colors']['background_color'] ?? '#1e3a5f';
$boxShadow = $data['colors']['box_shadow'] ?? '0 4px 12px rgba(30, 58, 95, 0.15)';
$linkTextColor = $data['links']['text_color'] ?? '#FFFFFF';
$linkHoverColor = $data['links']['hover_color'] ?? '#FF8600';
$linkActiveColor = $data['links']['active_color'] ?? '#FF8600';
$linkFontSize = $data['links']['font_size'] ?? '0.9rem';
$linkFontWeight = $data['links']['font_weight'] ?? '500';
$linkPadding = $data['links']['padding'] ?? '0.5rem 0.65rem';
$linkBorderRadius = $data['links']['border_radius'] ?? '4px';
$showUnderlineEffect = $data['links']['show_underline_effect'] ?? true;
$underlineColor = $data['links']['underline_color'] ?? '#FF8600';
// Estilos del navbar container
$navbarStyles = [
'background-color' => $bgColor . ' !important',
'box-shadow' => $boxShadow,
'padding' => $paddingVertical,
'transition' => 'all 0.3s ease',
];
if ($stickyEnabled) {
$navbarStyles['position'] = 'sticky';
$navbarStyles['top'] = '0';
$navbarStyles['z-index'] = $zIndex;
}
$css .= $this->cssGenerator->generate('.navbar', $navbarStyles);
// Efecto scrolled del navbar
$css .= "\n" . $this->cssGenerator->generate('.navbar.scrolled', [
'box-shadow' => '0 6px 20px rgba(30, 58, 95, 0.25)',
]);
// Estilos de los enlaces del navbar
$navLinkStyles = [
'color' => 'rgba(255, 255, 255, 0.9) !important',
'font-weight' => $linkFontWeight,
'position' => 'relative',
'padding' => $linkPadding . ' !important',
'transition' => 'all 0.3s ease',
'font-size' => $linkFontSize,
'white-space' => 'nowrap',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link', $navLinkStyles);
// Efecto de subrayado (::after pseudo-element)
if ($showUnderlineEffect) {
$css .= "\n.navbar .nav-link::after {";
$css .= "\n content: '';";
$css .= "\n position: absolute;";
$css .= "\n bottom: 0;";
$css .= "\n left: 50%;";
$css .= "\n transform: translateX(-50%) scaleX(0);";
$css .= "\n width: 80%;";
$css .= "\n height: 2px;";
$css .= "\n background: {$underlineColor};";
$css .= "\n transition: transform 0.3s ease;";
$css .= "\n}";
$css .= "\n.navbar .nav-link:hover::after {";
$css .= "\n transform: translateX(-50%) scaleX(1);";
$css .= "\n}";
}
// Estilos hover y focus de los enlaces
$navLinkHoverStyles = [
'color' => $linkHoverColor . ' !important',
'background-color' => 'rgba(255, 133, 0, 0.1)',
'border-radius' => $linkBorderRadius,
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link:hover, .navbar .nav-link:focus', $navLinkHoverStyles);
// Estilos de enlaces activos
$navLinkActiveStyles = [
'color' => $linkActiveColor . ' !important',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .nav-link.active, .navbar .nav-item.current-menu-item > .nav-link', $navLinkActiveStyles);
// Estilos del dropdown menu
$dropdownMaxHeight = $data['visual_effects']['dropdown_max_height'] ?? '300px';
$dropdownStyles = [
'background' => $data['visual_effects']['background_color'] ?? '#ffffff',
'border' => 'none',
'box-shadow' => $data['visual_effects']['shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.12)',
'border-radius' => $data['visual_effects']['border_radius'] ?? '8px',
'padding' => '0.5rem 0',
'max-height' => $dropdownMaxHeight,
'overflow-y' => 'auto',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-menu', $dropdownStyles);
// Hover en desktop para mostrar dropdown (sin necesidad de clic)
$css .= "\n@media (min-width: 992px) {";
$css .= "\n .navbar .dropdown:hover > .dropdown-menu {";
$css .= "\n display: block;";
$css .= "\n margin-top: 0;";
$css .= "\n }";
$css .= "\n .navbar .dropdown > .dropdown-toggle:active {";
$css .= "\n pointer-events: none;";
$css .= "\n }";
$css .= "\n}";
// Estilos de items del dropdown
$dropdownItemStyles = [
'color' => $data['visual_effects']['item_color'] ?? '#495057',
'padding' => $data['visual_effects']['item_padding'] ?? '0.625rem 1.25rem',
'transition' => 'all 0.3s ease',
'font-weight' => '500',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-item', $dropdownItemStyles);
// Estilos hover de items del dropdown
$dropdownItemHoverStyles = [
'background-color' => $data['visual_effects']['item_hover_background'] ?? 'rgba(255, 133, 0, 0.1)',
'color' => $linkHoverColor,
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .dropdown-item:hover, .navbar .dropdown-item:focus', $dropdownItemHoverStyles);
// Estilos del brand (texto)
$brandStyles = [
'color' => ($data['media']['brand_color'] ?? '#FFFFFF') . ' !important',
'font-weight' => '700',
'font-size' => $data['media']['brand_font_size'] ?? '1.5rem',
'transition' => 'color 0.3s ease',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .navbar-brand, .navbar .roi-navbar-brand', $brandStyles);
// Estilos hover del brand
$brandHoverStyles = [
'color' => ($data['media']['brand_hover_color'] ?? '#FF8600') . ' !important',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .navbar-brand:hover, .navbar .roi-navbar-brand:hover', $brandHoverStyles);
// Estilos del logo (imagen)
$logoStyles = [
'height' => $data['media']['logo_height'] ?? '40px',
'width' => 'auto',
];
$css .= "\n" . $this->cssGenerator->generate('.navbar .roi-navbar-logo', $logoStyles);
return $css;
}
private function buildMenu(array $data): string
{
$menuLocation = $data['behavior']['menu_location'] ?? 'primary';
$enableDropdowns = $data['behavior']['enable_dropdowns'] ?? true;
$mobileBreakpoint = $data['behavior']['mobile_breakpoint'] ?? 'lg';
$ulClass = 'navbar-nav mb-2 mb-lg-0';
$args = [
'theme_location' => $menuLocation === 'custom' ? '' : $menuLocation,
'menu' => $menuLocation === 'custom' ? ($data['behavior']['custom_menu_id'] ?? 0) : '',
'container' => false,
'menu_class' => $ulClass,
'fallback_cb' => '__return_false',
'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
'depth' => $enableDropdowns ? 2 : 1,
'walker' => new ROI_Bootstrap_Nav_Walker()
];
ob_start();
wp_nav_menu($args);
return ob_get_clean();
}
/**
* Obtiene las clases CSS de Bootstrap para visibilidad responsive
*
* Implementa tabla de decisión según especificación:
* - Desktop Y Mobile = null (visible en ambos)
* - Solo Desktop = 'd-none d-lg-block'
* - Solo Mobile = 'd-lg-none'
* - Ninguno = 'd-none' (oculto)
*
* @param bool $desktop Mostrar en desktop
* @param bool $mobile Mostrar en mobile
* @return string|null Clases CSS o null si visible en ambos
*/
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
{
if ($desktop && $mobile) {
return null; // Sin clases = visible siempre
}
if ($desktop && !$mobile) {
return 'd-none d-lg-block';
}
if (!$desktop && $mobile) {
return 'd-lg-none';
}
return 'd-none';
}
public function supports(string $componentType): bool
{
return $componentType === 'navbar';
}
}
/**
* Custom Walker for Bootstrap 5 Navigation
*
* RESPONSABILIDAD: Adaptar wp_nav_menu() a Bootstrap 5
*
* CARACTERÍSTICAS:
* - Clases Bootstrap 5 (.nav-item, .nav-link, .dropdown)
* - Atributos data-bs-toggle para dropdowns
* - Soporte para current-menu-item
*/
class ROI_Bootstrap_Nav_Walker extends Walker_Nav_Menu
{
public function start_lvl(&$output, $depth = 0, $args = null)
{
$indent = str_repeat("\t", $depth);
$output .= "\n$indent<ul class=\"dropdown-menu\">\n";
}
public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0)
{
$indent = ($depth) ? str_repeat("\t", $depth) : '';
$classes = empty($item->classes) ? [] : (array) $item->classes;
$classes[] = 'nav-item';
if ($args->walker->has_children) {
$classes[] = 'dropdown';
}
$class_names = join(' ', apply_filters('nav_menu_css_class', array_filter($classes), $item, $args, $depth));
$class_names = $class_names ? ' class="' . esc_attr($class_names) . '"' : '';
$id = apply_filters('nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth);
$id = $id ? ' id="' . esc_attr($id) . '"' : '';
$output .= $indent . '<li' . $id . $class_names . '>';
$atts = [];
$atts['title'] = !empty($item->attr_title) ? $item->attr_title : '';
$atts['target'] = !empty($item->target) ? $item->target : '';
$atts['rel'] = !empty($item->xfn) ? $item->xfn : '';
$atts['href'] = !empty($item->url) ? $item->url : '';
if ($depth === 0) {
$atts['class'] = 'nav-link';
if ($args->walker->has_children) {
$atts['class'] .= ' dropdown-toggle';
// Only add data-bs-toggle if no real URL (allows click navigation on desktop)
// CSS hover handles showing dropdown, data-bs-toggle only needed for mobile
$url = !empty($item->url) ? $item->url : '';
if (empty($url) || $url === '#' || $url === '#!') {
$atts['data-bs-toggle'] = 'dropdown';
}
$atts['role'] = 'button';
$atts['aria-expanded'] = 'false';
}
} else {
$atts['class'] = 'dropdown-item';
}
if (in_array('current-menu-item', $classes)) {
$atts['class'] .= ' active';
}
$atts = apply_filters('nav_menu_link_attributes', $atts, $item, $args, $depth);
$attributes = '';
foreach ($atts as $attr => $value) {
if (!empty($value)) {
$value = ('href' === $attr) ? esc_url($value) : esc_attr($value);
$attributes .= ' ' . $attr . '="' . $value . '"';
}
}
$title = apply_filters('the_title', $item->title, $item->ID);
$title = apply_filters('nav_menu_item_title', $title, $item, $args, $depth);
$item_output = $args->before;
$item_output .= '<a' . $attributes . '>';
$item_output .= $args->link_before . $title . $args->link_after;
$item_output .= '</a>';
$item_output .= $args->after;
$output .= apply_filters('walker_nav_menu_start_el', $item_output, $item, $depth, $args);
}
}