1<#include "${templatesPath}/C360-MACROS-NAVEGACION" />
2<#assign textoNuevaVentana = languageUtil.get(locale, "opens-new-window") />
3
4<nav class="navbar navigation-horizontal">
5 <div class="container-xxl pl-0 d-flex align-items-center">
6 <button
7 type="button"
8 class="scroll-arrow-wrapper scroll-arrow-left-wrapper scroll-arrow scroll-arrow-left gva-btn gva-btn-ghost-primary gva-btn-icon border-0"
9 aria-label="Ver elementos anteriores del menú"
10 aria-controls="main-navigation-list">
11 <i class="fa-solid fa-angle-left" aria-hidden="true"></i>
12 </button>
13
14 <ul id="main-navigation-list" class="navbar-nav scroll-horizontal d-flex">
15 <#assign navItems = entries />
16 <#list navItems as navItem>
17 <#if navItem.hasBrowsableChildren() && !(navItem.getLayout().getType() == "link_to_layout")>
18
19 <li class="dropdown <#if navItem.isChildSelected() || navItem.isSelected()>active</#if>">
20 <a class="dropdown-toggle gva-btn gva-btn-ghost-primary link-nav transition-underline" href="#" id="navbarDropdown_${navItem.getLayoutId()}" role="button" aria-haspopup="true" aria-expanded="false" aria-controls="submenu-${navItem.getLayoutId()}">
21 ${navItem.getName()}
22 <i class="fa-solid fa-angle-down" aria-hidden="true"></i>
23 </a>
24 <div id="submenu-${navItem.getLayoutId()}" class="dropdown-menu" aria-labelledby="navbarDropdown_${navItem.getLayoutId()}">
25 <div class="container-xxl dropdown-close">
26 <button class="close" aria-label="<@liferay.language key="close" />">
27 <span aria-hidden="true">×</span>
28 </button>
29 </div>
30 <ul class="container-xxl child-list">
31
32 <#list navItem.getChildren() as childNavItem>
33
34 <#if childNavItem.hasBrowsableChildren() && (childNavItem.getLayout().getType() == "link_to_layout" || childNavItem.getTarget()?contains("group-pages") )>
35 <#assign ariaCurrentChild = (childNavItem.isSelected() || childNavItem.isChildSelected())?then('aria-current="page"', '') />
36 <li class="d-flex flex-column m-0 <#if childNavItem.getTarget()?contains("break-before")>break-before</#if>">
37 <a class="gva-btn gva-btn-ghost-primary" <@pintarHrefTarget childNavItem themeDisplay/> ${ariaCurrentChild}><span>${childNavItem.getName()}</span></a>
38 <ul class="p-0 ml-4">
39 <#list childNavItem.getChildren() as childChildNavItem>
40 <#assign ariaCurrentChildChild = (childChildNavItem.isSelected() || childChildNavItem.isChildSelected())?then('aria-current="page"', '') />
41 <li class="d-flex align-items-center add-link-icons <#if childChildNavItem.isChildSelected() || childChildNavItem.isSelected()>active</#if>">
42 <i class="fa-solid fa-square-full" aria-hidden="true"></i>
43 <a class="gva-btn gva-btn-ghost-primary d-flex" <@pintarHrefTarget childChildNavItem themeDisplay/>
44 ${ariaCurrentChildChild}
45 <#if childChildNavItem.getTarget()?has_content && childChildNavItem.getTarget()?contains('target="_blank"')>
46 aria-label="${childChildNavItem.getName()?html} - ${textoNuevaVentana?html}"
47 </#if>
48 >${childChildNavItem.getName()}</a>
49 </li>
50 </#list>
51 </ul>
52 </li>
53
54 <#else>
55 <#assign ariaCurrentChild = (childNavItem.isSelected() || childNavItem.isChildSelected())?then('aria-current="page"', '') />
56 <li class="d-flex align-items-center add-link-icons <#if childNavItem.isChildSelected() || childNavItem.isSelected()>active</#if>">
57 <i class="fa-solid fa-square-full" aria-hidden="true"></i>
58 <a class="gva-btn gva-btn-ghost-primary d-flex" <@pintarHrefTarget childNavItem themeDisplay/>
59 ${ariaCurrentChild}
60 <#if childChildNavItem.getTarget()?has_content && childChildNavItem.getTarget()?contains('target="_blank"')>
61 aria-label="${childChildNavItem.getName()?html} - ${textoNuevaVentana?html}"
62 </#if>
63 >${childNavItem.getName()}</a>
64 </li>
65 </#if>
66
67 </#list>
68 </ul>
69 </div>
70 </li>
71 <#else>
72 <#assign ariaCurrent = navItem.isSelected()?then('aria-current="page"', '') />
73 <li <#if navItem.isSelected()>class="active"</#if>>
74 <a class="gva-btn gva-btn-ghost-primary link-nav transition-underline" <@pintarHrefTarget navItem themeDisplay/> ${ariaCurrent}>${navItem.getName()}</a>
75 </li>
76 </#if>
77 </#list>
78 </ul>
79
80 <button
81 type="button"
82 class="scroll-arrow-wrapper scroll-arrow-right-wrapper scroll-arrow scroll-arrow-right gva-btn gva-btn-ghost-primary gva-btn-icon border-0"
83 aria-label="Ver más elementos del menú"
84 aria-controls="main-navigation-list">
85 <i class="fa-solid fa-angle-right" aria-hidden="true"></i>
86 </button>
87 </div>
88</nav>
89
90<script>
91 <#--
92 ===================================================================================
93 FUNCIONALIDADES DEL MENÚ
94 ===================================================================================
95 Este script controla la interacción del menú. Si se modifica algo, se puede seguir esta
96 lista para verificar que no se ha roto nada. Si se modifican o añaden funcionalidades hay
97 que actualizar este comentario.
98
99 1. NAVEGACIÓN Y CARGA (Liferay SPA)
100 Carga Inicial: El menú carga correctamente al entrar en la página.
101 Navegación SPA: Al navegar a otra página sin recargar, el menú carga y que los eventos NO se duplican.
102
103 2. MENÚS DESPLEGABLES (Dropdowns)
104 Apertura: Click en un ítem abre su submenú y oscurece el fondo (Overlay). Si abres un segundo menú,
105 el primero se cierra automáticamente.
106 Cierre: El menú debe cerrarse al:
107 - Hacer click fuera del menú (en el cuerpo de la página).
108 - Pulsar la tecla 'ESC' (Accesibilidad).
109 - Pulsar el botón de cerrar 'X' dentro del submenú.
110
111 3. SCROLL HORIZONTAL (Flechas)
112 Visibilidad: Si el menú es más ancho que la pantalla, aparecen flechas laterales.
113 Acción: Click en flechas mueve el menú suavemente a izq/der.
114 Resize: Al cambiar el tamaño de la ventana, las flechas aparecen/desaparecen según necesidad.
115
116 4. EFECTOS DE SCROLL VERTICAL (Sticky Header)
117 Al bajar (Scroll Down): El menú se contrae (clase .scrolled) y se quita el overlay.
118 Al subir (Scroll Up): El menú recupera su estado original.
119 * Nota: Esta funcionalidad se aplica en la cabecera que no es gva. En cabecera gva el
120 menú desplegado se muestra siempre.
121
122 5. OTRAS FUNCIONALIDADES
123 Margen Superior: El contenido de la página (#content) baja automáticamente según
124 la altura de la cabecera (evita que el menú tape el título).
125 Ítem Activo: El menú marca como 'active' la sección actual comparando su texto
126 con el del Breadcrumb (Migas de pan), solo en cabeceras que utilizan "navigation.menu.id".
127
128 ===================================================================================
129 -->
130 (function() {
131
132 <#--
133 PREVENCIÓN DE DUPLICIDAD DE EVENTOS (LIFERAY SPA)
134 Comprobamos si este script ya ha sido inicializado en el objeto 'window'.
135 Esto evita que, al navegar entre páginas (sin recarga completa), se vuelvan
136 a añadir listeners globales, lo que duplicaría la ejecución y degradaría el rendimiento.
137 Si la bandera ya existe "window.__mainNavigationInitialized = true", significa que este
138 código ya se cargó previamente.
139 -->
140 if (window.__mainNavigationInitialized) {
141 return;
142 }
143 window.__mainNavigationInitialized = true;
144
145 function updateScrollArrows() {
146 setTimeout(() => {
147 const scrollContainer = document.querySelector('.navbar-nav.scroll-horizontal');
148 const leftArrow = document.querySelector('.scroll-arrow-left-wrapper');
149 const rightArrow = document.querySelector('.scroll-arrow-right-wrapper');
150
151 if (!scrollContainer || !leftArrow || !rightArrow) return;
152
153 const scrollLeft = scrollContainer.scrollLeft;
154 const scrollWidth = scrollContainer.scrollWidth;
155 const clientWidth = scrollContainer.clientWidth;
156
157 const rightArrowWidth = rightArrow.getBoundingClientRect().width;
158
159 const hasScroll = scrollWidth > clientWidth;
160 const canScrollLeft = scrollLeft > 0;
161 const canScrollRight = scrollLeft + clientWidth < scrollWidth - rightArrowWidth;
162
163 // Mostrar u ocultar las flechas
164 leftArrow.classList.toggle('scroll-visible', hasScroll && canScrollLeft);
165 rightArrow.classList.toggle('scroll-visible', hasScroll && canScrollRight);
166 }, 50); // Pequeño retraso para asegurar que los cálculos del DOM son precisos
167
168 const scroll = document.querySelector('.scroll-horizontal');
169
170 // Desplazamiento con las flechas
171 document.querySelector('.scroll-arrow-left')?.addEventListener('click', (e) => {
172
173 e.preventDefault();
174 scroll?.scrollBy({left: -250, behavior: 'smooth'});
175
176 // Llama a updateScrollArrows después de un breve retraso para que el scroll termine
177 setTimeout(updateScrollArrows, 300);
178 });
179
180 document.querySelector('.scroll-arrow-right')?.addEventListener('click', (e) => {
181 e.preventDefault();
182 scroll?.scrollBy({left: 250, behavior: 'smooth'});
183
184 // Llama a updateScrollArrows después de un breve retraso para que el scroll termine
185 setTimeout(updateScrollArrows, 300);
186 });
187
188 // Ejecutar al cargar y al cambiar de tamaño o hacer scroll
189 window.addEventListener('resize', updateScrollArrows);
190 document.querySelector('.scroll-horizontal')?.addEventListener('scroll', updateScrollArrows);
191 }
192
193 document.addEventListener("DOMContentLoaded", updateScrollArrows);
194 Liferay.on('endNavigate', updateScrollArrows);
195 <#-- Funcion para resetear el valor del atributo aria-expanded en el menu -->
196 function setMainNavigationAriaExpanded(dropdownToggle, isExpanded) {
197 if (dropdownToggle) {
198 dropdownToggle.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
199 }
200 }
201
202 <#-- Funcion para controlar el valor del atributo aria-expanded en el menu -->
203 function resetMainNavigationAriaExpanded() {
204 document.querySelectorAll('.main-navigation-wrapper .navbar-nav .dropdown-toggle[aria-expanded]').forEach(dropdownToggle => {
205 setMainNavigationAriaExpanded(dropdownToggle, false);
206 });
207 }
208
209 <#--
210 FUNCION: closeMenuDropdown
211 DESCRIPCION: Cierra el menú desplegado, elimina las clases de estado 'show/open etc...'
212 y gestiona la visibilidad del overlay. También recalcula la altura si no es 'gva'.
213 -->
214 function closeMenuDropdown() {
215 document.body.classList.remove('main-navigation-dropdown-menu-show');
216
217 document.querySelectorAll('.main-navigation-wrapper .navbar-nav .dropdown-menu, .main-navigation-wrapper .navbar-nav .dropdown').forEach(element => {
218 element.classList.remove('show');
219 });
220 document.querySelectorAll('.main-navigation-wrapper').forEach(mainNavigationWrapper => {
221 mainNavigationWrapper.classList.remove('open');
222 });
223 resetMainNavigationAriaExpanded();
224
225 if (!document.body.classList.contains('gva')) {
226 const mainNavigationWrapper = document.querySelector('.main-navigation-wrapper');
227 if (mainNavigationWrapper) {
228 mainNavigationWrapper.style.removeProperty('height');
229 mainNavigationWrapper.style.height = mainNavigationWrapper.offsetHeight + 'px';
230 }
231 }
232
233 document.getElementById('overlay')?.classList.remove('nav');
234 }
235
236 <#--
237 FUNCION: navDropdownsClick
238 DESCRIPCION: Añade los listeners de 'click' a los elementos desplegables.
239 Gestiona la apertura y cálculos de altura del menú desplgado.
240 -->
241 function navDropdownsClick() {
242 const navDropdowns = document.querySelectorAll('.main-navigation-wrapper .navbar-nav .dropdown-toggle');
243 navDropdowns.forEach(dropdown => {
244 dropdown.addEventListener('click', (event) => {
245 event.preventDefault();
246
247 <#-- Detiene la propagación para que no salte el evento click del body -->
248 event.stopPropagation();
249
250 <#-- Limpiamos y cerramos cualquier menú que pudiera estar abierto antes de desplegar el nuevo -->
251 closeMenuDropdown();
252
253 const parent = event.currentTarget.closest('.main-navigation-wrapper .dropdown');
254 const isActive = parent?.classList.contains('show');
255
256 <#-- Si el elemento pulsado NO estaba activo, procedemos a abrirlo -->
257 if (!isActive && parent) {
258 document.body.classList.add('main-navigation-dropdown-menu-show');
259 parent?.classList.add('show');
260
261 const dropdownMenu = parent.querySelector('.dropdown-menu');
262 if (dropdownMenu) {
263 dropdownMenu.classList.add('show');
264 dropdownMenu.style.visibility = 'visible';
265 }
266 setMainNavigationAriaExpanded(event.currentTarget, true);
267 const mainNavigationWrapper = document.querySelector('.main-navigation-wrapper');
268 const overlay = document.getElementById('overlay');
269
270 mainNavigationWrapper?.classList.add('open');
271 overlay?.classList.add('nav');
272
273 <#-- Codigo en js para los calculos por la funcionalidad de la cabecera fixed -->
274 if (!document.body.classList.contains('gva')) {
275 <#-- Usamos setTimeout para esperar por seguridad a que el DOM renderice la clase .show antes de medir alturas -->
276 setTimeout(() => {
277 const dropdownMenuShow = document.querySelector('.dropdown-menu.show');
278 const mainNavigationWrapperOpen = document.querySelector('.main-navigation-wrapper.open');
279
280 if (dropdownMenuShow && mainNavigationWrapperOpen) {
281 const dropdownHeight = dropdownMenuShow.offsetHeight;
282 mainNavigationWrapperOpen.style.height = dropdownHeight + mainNavigationWrapper.offsetHeight + 'px';
283
284 <#-- Ajuste dinámico para permitir scroll dentro del dropdown si es muy alto -->
285 const dropdownOpen = document.querySelector('.main-navigation-wrapper .dropdown.show .dropdown-menu.show');
286 if (dropdownOpen) {
287 dropdownOpen.style.maxHeight = dropdownHeight + 'px';
288 }
289 }
290 }, 100);
291 }
292 <#-- Fin -->
293
294 }
295 });
296 });
297 }
298
299 <#-- EVENTO: Click en el Body. Cierra el menú si se hace click fuera de la barra de navegación -->
300 document.body.addEventListener('click', (event) => {
301 const openMenu = document.querySelector('.main-navigation-wrapper .navbar-nav .dropdown-menu.show');
302 if (openMenu !== null) {
303 if (!event.target.closest('.navbar-nav')) {
304 closeMenuDropdown();
305 }
306 }
307 });
308
309 <#-- EVENTO: Tecla Escape. Accesibilidad para cerrar el menú con teclado -->
310 document.body.addEventListener('keydown', (event) => {
311 if (event.key === 'Escape' || event.keyCode === 27) {
312 const openMenu = document.querySelector('.main-navigation-wrapper .navbar-nav .dropdown-menu.show');
313 if (openMenu !== null) {
314 if (event.target.closest('.navbar-nav')) {
315 closeMenuDropdown();
316 }
317 }
318 }
319 });
320
321 <#--
322 FUNCIONALIDAD SCROLL: Controla la clase .scrolled y el overlay cuando el usuario baja la página.
323 Pliega el menú si está desplegado y la barra navegación cuando el usuario baja, lo vuelve a desplegar
324 en el mismo estado cuanto vuelve arriba
325 -->
326 if (!document.body.classList.contains('gva')) {
327 window.addEventListener('scroll', () => {
328 const overlay = document.getElementById('overlay');
329 const mainNavigationWrapper = document.querySelector('.main-navigation-wrapper');
330 if (window.scrollY > 42) {
331 mainNavigationWrapper?.classList.add('scrolled');
332 overlay?.classList.remove('nav');
333 } else {
334 mainNavigationWrapper?.classList.remove('scrolled');
335 const isMenuOpen = document.querySelector('.main-navigation-wrapper .navbar-nav .dropdown-menu.show');
336 if (isMenuOpen) {
337 overlay?.classList.add('nav');
338 }
339 }
340 });
341 }
342
343 <#--
344 FUNCION: setActiveMenuItemFromBreadcrumb
345 DESCRIPCION: Compara los elementos del breadcrumb con el menú principal para marcar como 'active' en el menú
346 la sección correspondiente. (casos como GVA o Sede) Usa jQuery.
347 -->
348 function setActiveMenuItemFromBreadcrumb() {
349 <#assign navigation_menu_id = themeDisplay.getThemeSetting("navigation.menu.id") >
350 <#if navigation_menu_id?has_content>
351 $("#breadcrumb .breadcrumb .breadcrumb-item a").each(function (index) {
352 var elementoBreadcrumb = $(this);
353 if (elementoBreadcrumb) {
354 var tituloElementoBreadcrumb = elementoBreadcrumb.attr("title");
355 if (tituloElementoBreadcrumb) {
356 tituloElementoBreadcrumb = tituloElementoBreadcrumb.toLowerCase().trim();
357 $("header#banner .main-navigation-wrapper.gva nav.navbar ul.navbar-nav>li a").each(function (index) {
358
359 var elementoNavegacion = $(this);
360 if (elementoNavegacion) {
361 var tituloNavegacion = elementoNavegacion.text().toLowerCase().trim()
362 if (tituloNavegacion && tituloNavegacion === tituloElementoBreadcrumb) {
363 $(this).parent().addClass("active");
364 $(this).attr("aria-current", "page");
365 }
366 }
367 });
368 }
369 }
370 });
371 </#if>
372 }
373
374 <#--
375 FUNCION: calcContentMarginTop
376 DESCRIPCION: Las cabeceras están posicionadas como fixed, por lo que es necesario aplicar un margen superior al contenido.
377 Dado que las cabeceras pueden tener alturas diferentes, se calcula dinámicamente la altura de la cabecera y se utiliza
378 ese valor como margen superior del contenido.
379 -->
380 function calcContentMarginTop(){
381 const banner = document.getElementById('banner');
382 const bannerHeight = banner ? banner.offsetHeight : 0;
383 const content = document.getElementById('content');
384 const controlMenuContainer = document.querySelector('.control-menu-container');
385 const heightControlMenuContainer = controlMenuContainer ? controlMenuContainer.offsetHeight : 0;
386
387 if (content) {
388 content.style.marginTop = bannerHeight - heightControlMenuContainer + 'px';
389 }
390 }
391
392 <#--
393 FUNCION: initMainNavigation
394 DESCRIPCION: Inicializador principal. Configura alturas iniciales, márgenes del contenido
395 y dispara la asignación de eventos.
396 -->
397 function initMainNavigation() {
398
399 const mainNavigationWrapper = document.querySelector('.main-navigation-wrapper');
400
401 if (mainNavigationWrapper) {
402
403 if (!document.body.classList.contains('gva')) {
404 <#-- Antes de ajustar el alto del mainNavigationWrapper hay que limpiarlo de clases
405 por el caso de cuando se refresca una página en con el evento scroll activado, para poder
406 calcular el alto correcto-->
407 mainNavigationWrapper.classList.remove('scrolled');
408 mainNavigationWrapper.style.removeProperty('height');
409
410 mainNavigationWrapper.style.height = mainNavigationWrapper.offsetHeight + 'px';
411 }
412
413 calcContentMarginTop();
414
415 navDropdownsClick();
416
417 <#-- Listener para el botón explícito de "Cerrar" dentro de los menús -->
418 const closeButtons = document.querySelectorAll('.dropdown-close .close');
419
420 closeButtons.forEach(closeBtn => {
421 closeBtn.addEventListener('click', event => {
422 const openMenu = document.querySelector('.main-navigation-wrapper .navbar-nav .dropdown-menu.show');
423 if (openMenu !== null) {
424 if (event.target.closest('.navbar-nav')) {
425 closeMenuDropdown();
426 }
427 }
428 });
429 });
430
431 setActiveMenuItemFromBreadcrumb();
432
433 }
434 }
435
436 <#--
437 INICIALIZACION:
438 - load: Carga inicial normal cuando todo los recursos están cargados.
439 - endNavigate: Evento específico de Liferay SPA (Single Page Application) para reinicializar al navegar sin recargar.
440 -->
441 window.addEventListener("load", initMainNavigation);
442 Liferay.on('endNavigate', initMainNavigation);
443
444 })();
445</script>