- Apply same fix to NavbarRenderer's walker class - Only add data-bs-toggle=dropdown for items without real URL - Fixes Buscador General link navigation
370 lines
14 KiB
PHP
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);
|
|
}
|
|
}
|