1<#include "${templatesPath}/C360-MACROS-NAVEGACION" />
2
3<nav class="navbar-mobile"></nav>
4<#assign htmlOutput = "" />
5<#macro buildNavigation
6 branchNavItems
7 cssClass
8 displayDepth
9 includeAllChildNavItems
10 navItemLevel
11 navItems
12 currentItemName
13 currentItemUrl
14 target
15>
16 <#if navItems?has_content && ((displayDepth == 0) || (navItemLevel <= displayDepth))>
17 <#if (navItemLevel > 1)>
18 <#assign htmlOutput = htmlOutput + '<ul class="${cssClass} level-${navItemLevel}">' />
19 <#assign htmlOutput = htmlOutput + '<li class="back-btn d-flex"><a href="" class="gva-btn back"><i class="fa-solid fa-chevron-left"></i></a><a ${target} class="gva-btn w-100" href="${currentItemUrl}">${currentItemName}</a></li>' />
20 <#else>
21 <#assign htmlOutput = htmlOutput + '<ul class="${cssClass} level-${navItemLevel}">' />
22 </#if>
23 <#list navItems as navItem>
24 <#assign nav_item_css_class = "" />
25 <#if navItem.isChildSelected() || navItem.isSelected()>
26 <#assign nav_item_css_class = "selected active"/>
27 </#if>
28 <#assign htmlOutput = htmlOutput + '<li class="${nav_item_css_class} layout-${navItem.getLayoutId()} add-link-icons">' />
29 <#if navItem.hasChildren()>
30 <#assign htmlOutput = htmlOutput + '<a rel="nofollow" class="gva-btn gva-btn-ghost-primary link-nav ${nav_item_css_class} submenu-toggle" href="${getHref(navItem,themeDisplay)}" ${navItem.getTarget()}>${navItem.getName()}<i class="fa-solid fa-chevron-right"></i></a>' />
31 <#else>
32 <#assign htmlOutput = htmlOutput + '<a rel="nofollow" class="gva-btn gva-btn-ghost-primary link-nav ${nav_item_css_class}" href="${getHref(navItem,themeDisplay)}" ${navItem.getTarget()}>${navItem.getName()}</a>' />
33 </#if>
34
35 <#if includeAllChildNavItems || navItem.isInNavigation(branchNavItems)>
36 <@buildNavigation
37 branchNavItems=branchNavItems
38 cssClass=cssClass
39 displayDepth=displayDepth
40 includeAllChildNavItems=includeAllChildNavItems
41 navItemLevel=(navItemLevel + 1)
42 navItems=navItem.getChildren()
43 currentItemName=navItem.getName()
44 currentItemUrl=navItem.getRegularURL()
45 target=navItem.getTarget()
46 />
47 </#if>
48 <#assign htmlOutput = htmlOutput + '</li>' />
49 </#list>
50 <#assign htmlOutput = htmlOutput + '</ul>' />
51 </#if>
52</#macro>
53
54<#if !entries?has_content>
55 <#if themeDisplay.isSignedIn()>
56 <#assign htmlOutput = htmlOutput + '<div class="alert alert-info"><@liferay.language key="there-are-no-menu-items-to-display" /></div>' />
57 </#if>
58<#else>
59 <#assign htmlOutput = htmlOutput + '<div class="list-menu">' />
60 <@buildNavigation
61 branchNavItems=branchNavItems
62 cssClass="layouts"
63 displayDepth=displayDepth
64 includeAllChildNavItems=true
65 navItemLevel=1
66 navItems=entries
67 currentItemName=""
68 currentItemUrl=""
69 target=""
70 />
71 <#assign htmlOutput = htmlOutput + '</div>' />
72</#if>
73
74
75
76<script>
77 (function() {
78
79 <#--
80 ===================================================================================
81 FUNCIONALIDADES DEL MENU MOVIL
82 ===================================================================================
83 Este script controla la interaccion del menu movil. Si se modifica algo, se puede seguir esta
84 lista para verificar que no se ha roto nada. Si se modifican o anaden funcionalidades hay
85 que actualizar este comentario.
86
87 1. NAVEGACION Y CARGA (Liferay SPA)
88 Carga Inicial: El menu movil carga correctamente al entrar en la pagina.
89 Navegacion SPA: Al navegar sin recargar, el menu se reinicializa sin duplicar eventos
90 (eventos globales una sola vez, eventos del wrapper se vuelven a asignar si cambia el DOM).
91
92 2. SUBMENUS (NIVELES)
93 Apertura: Click en item con submenu activa el siguiente nivel y oculta enlaces del nivel actual.
94 Retroceso: Click en el boton de volver activa el nivel anterior y recalcula alturas.
95 Importante: El click de submenu/back se maneja dentro del wrapper para evitar navegacion del enlace.
96
97 3. BOTON HAMBURGUESA Y OVERLAY
98 Apertura/Cierre: Click en el boton muestra/oculta el menu y alterna los iconos.
99 Overlay: Se muestra/oculta con la clase .nav-mobile.
100 HTML: Se inserta el HTML del menu en cada apertura (menu actualizado en SPA).
101
102 4. CIERRE DEL MENU
103 Click fuera: Cierra el menu si se hace click fuera del wrapper o el boton.
104 ESC: Cierra el menu si esta abierto.
105
106 5. ESTADO VISUAL Y ALTURAS
107 Alturas: Ajuste dinamico del nivel 1 segun el submenu activo.
108 Nivel actual: Al abrir, activa el UL mas profundo con items selected/active y ajusta el alto.
109 Iconos: Alterna los iconos en el boton segun el estado de menu cerrado/abierto.
110
111 6. EXTRAS Y ACTIVO
112 Clonado: Anade area privada, logo/nombre y selector de idiomas al menu movil.
113 Item Activo: Marca como activo el item segun las migas de pan (jQuery),
114 solo en cabeceras que utilizan "navigation.menu.id".
115
116 ===================================================================================
117 -->
118
119 <#-- Variable que contiene el HTML de navegacion -->
120 const navigationHTML = "${htmlOutput?json_string}";
121 window.__mainNavigationMobileHTML = navigationHTML;
122
123 <#--
124 PREVENCION DE DUPLICIDAD DE EVENTOS (LIFERAY SPA)
125 Comprobamos si este script ya ha sido inicializado en el objeto 'window'.
126 Esto evita que, al navegar entre paginas (sin recarga completa), se vuelvan
127 a anadir listeners globales, lo que duplicaria la ejecucion y degradaria el rendimiento.
128 Si la bandera ya existe "window.__mainNavigationMobileInitialized = true", significa que este
129 codigo ya se cargo previamente.
130 -->
131 if (window.__mainNavigationMobileInitialized) {
132 return;
133 }
134 window.__mainNavigationMobileInitialized = true;
135
136 <#--
137 FUNCION: getLevelFromElement
138 DESCRIPCION: Obtiene el nivel (level-n) del elemento (li) clicado a partir de su UL padre.
139 -->
140 function getLevelFromElement(element) {
141 const parentUl = element ? element.closest('ul') : null;
142 if (!parentUl || !parentUl.className) {
143 return null;
144 }
145 const match = parentUl.className.match(/level-(\d+)/);
146 return match ? parseInt(match[1], 10) : null;
147 }
148
149 <#--
150 FUNCION: handleSubmenuClick
151 DESCRIPCION: Gestiona el click en un submenu, activa el nivel siguiente y ajusta alturas.
152 -->
153 function handleSubmenuClick(event, level, target) {
154 event.preventDefault();
155
156 <#-- Oculta los a del nivel actual (el nivel que se oculta) ya que el icono de enlace externo siempre se veia, se anade settimeout para evitar un efecto raro al cambiar de nivel -->
157 setTimeout(function() {
158 document.querySelectorAll('.navbar-mobile ul.level-' + level + ' > li > a').forEach(function(node) {
159 node.style.display = 'none';
160 });
161 }, 200);
162
163 const parentLi = target ? target.closest('li') : null;
164 const nextLevel = parentLi ? parentLi.querySelector('ul.level-' + (level + 1)) : null;
165
166 <#-- Ajusta el alto de cada submenu -->
167 const level1 = document.querySelector('.navbar-mobile ul.level-1');
168 if (level1 && nextLevel) {
169 level1.style.height = nextLevel.offsetHeight + 'px';
170 }
171
172 if (nextLevel) {
173 nextLevel.classList.add('active');
174 }
175 }
176
177 <#--
178 FUNCION: handleBackClick
179 DESCRIPCION: Gestiona el click en volver, desactiva el nivel actual y activa el nivel previo y ajusta alturas.
180 -->
181 function handleBackClick(event, level) {
182 event.preventDefault();
183 const currentLevel = document.querySelector('.navbar-mobile ul.level-' + level + '.active');
184 const previousLevel = currentLevel ? currentLevel.closest('ul.level-' + (level - 1)) : null;
185
186 <#-- Vuelve a mostrar los a del nivel que se van a visualizar -->
187 document.querySelectorAll('.navbar-mobile ul.level-' + (level - 1) + ' > li > a').forEach(function(node) {
188 node.style.display = 'flex';
189 });
190 if (previousLevel && currentLevel) {
191 currentLevel.classList.remove('active');
192
193 <#-- Ajusta el alto de cada submenu -->
194 const level1 = document.querySelector('.navbar-mobile ul.level-1');
195 if (level1) {
196 if (level == 2) {
197 level1.style.height = '';
198 } else {
199 level1.style.height = previousLevel.offsetHeight + 'px';
200 }
201 }
202 }
203 }
204
205 <#--
206 FUNCION: setOverlay
207 DESCRIPCION: Muestra u oculta el overlay del menu movil.
208 -->
209 function setOverlay(isOpen, overlay) {
210 if (!overlay) {
211 return;
212 }
213 if (isOpen) {
214 overlay.classList.add('nav-mobile');
215 } else {
216 overlay.classList.remove('nav-mobile');
217 }
218 }
219
220 <#--
221 FUNCION: setHamburgerState
222 DESCRIPCION: Actualiza estado del boton hamburguesa, iconos y visibilidad del wrapper.
223 -->
224 function setHamburgerState(isOpen, hamburgerButton, navWrapper, faBars, faXmark) {
225 if (!hamburgerButton) {
226 return;
227 }
228
229 if (isOpen) {
230 hamburgerButton.classList.add('active');
231 } else {
232 hamburgerButton.classList.remove('active');
233 }
234
235 if (navWrapper) {
236 navWrapper.style.display = isOpen ? 'block' : 'none';
237 }
238 if (faBars) {
239 faBars.style.display = isOpen ? 'none' : 'block';
240 }
241 if (faXmark) {
242 faXmark.style.display = isOpen ? 'block' : 'none';
243 }
244 }
245
246 <#--
247 FUNCION: addMenuExtras
248 DESCRIPCION: Clona e inserta en el menu movil los bloques auxiliares (logo, nombre, area privada e idiomas) si existen.
249 -->
250 function addMenuExtras(navbarListMenu, logoDer, nameParent, privateArea, languageSelector) {
251 if (navbarListMenu && logoDer) {
252 navbarListMenu.appendChild(logoDer.cloneNode(true));
253 }
254 if (navbarListMenu && nameParent) {
255 navbarListMenu.appendChild(nameParent.cloneNode(true));
256 }
257 if (navbarListMenu && privateArea) {
258 navbarListMenu.appendChild(privateArea.cloneNode(true));
259 }
260 if (navbarListMenu && languageSelector) {
261 navbarListMenu.appendChild(languageSelector.cloneNode(true));
262 }
263 }
264
265 <#--
266 FUNCION: setActiveMenuItemFromBreadcrumb
267 DESCRIPCION: Compara los elementos del breadcrumb con el menú principal para marcar como 'active' en el menú
268 la sección correspondiente. (casos como GVA o Sede) Usa jQuery.
269 -->
270 function setActiveMenuItemFromBreadcrumb() {
271 <#assign navigation_menu_id = themeDisplay.getThemeSetting("navigation.menu.id") >
272 <#if navigation_menu_id?has_content>
273 <#-- Codigo en js para ACTIVAR las opciones del menu que tienen el mismo nombre que alguno de los elementos del camino de migas -->
274 $("#breadcrumb .breadcrumb .breadcrumb-item a").each(function( index ) {
275 var elementoBreadcrumb = $( this );
276 if (elementoBreadcrumb){
277 var tituloElementoBreadcrumb = elementoBreadcrumb.attr("title");
278 if (tituloElementoBreadcrumb){
279 tituloElementoBreadcrumb = tituloElementoBreadcrumb.toLowerCase().trim();
280 $("header#banner .main-navigation-mobile-wrapper nav.navbar-mobile .list-menu a").each(function( index ) {
281
282 var elementoNavegacion = $( this );
283 if (elementoNavegacion){
284 var tituloNavegacion = elementoNavegacion.text().toLowerCase().trim()
285 if (tituloNavegacion && tituloNavegacion === tituloElementoBreadcrumb){
286 $( this ).parent().addClass("selected");
287 $( this ).parent().addClass("active");
288 }
289 }
290 });
291 }
292 }
293 });
294 </#if>
295 }
296
297 <#--
298 FUNCION: closeMobileMenuDropdown
299 DESCRIPCION: Cierra el menu movil, resetea el boton y oculta el overlay.
300 -->
301 function closeMobileMenuDropdown() {
302 const navWrapper = document.querySelector('.main-navigation-mobile-wrapper');
303 const hamburgerButton = document.querySelector('.hamburger-button-mobile');
304 const overlay = document.getElementById('overlay');
305
306 if (!hamburgerButton) {
307 return;
308 }
309
310 const faBars = hamburgerButton.querySelector('.fa-bars');
311 const faXmark = hamburgerButton.querySelector('.fa-xmark');
312
313 setHamburgerState(false, hamburgerButton, navWrapper, faBars, faXmark);
314
315 setOverlay(false, overlay);
316
317 }
318
319 <#--
320 FUNCION: onSubmenuToggleClick
321 DESCRIPCION: Handler del click en submenu, evita navegacion (que se recargue la pagina).
322 -->
323 function onSubmenuToggleClick(event) {
324 const target = event.target.closest('.navbar-mobile .submenu-toggle');
325 if (!target) {
326 return;
327 }
328
329 <#-- Evita la navegacion por defecto si el item es un submenu -->
330 event.preventDefault();
331 event.stopPropagation();
332
333 if (!event.target.closest('.main-navigation-mobile-wrapper')) {
334 return;
335 }
336
337 <#-- Obtiene el nivel del submenu actual -->
338 const level = getLevelFromElement(target);
339 if (!level) {
340 return;
341 }
342 handleSubmenuClick(event, level, target);
343 }
344
345 <#--
346 FUNCION: onBackButtonClick
347 DESCRIPCION: Handler del click en volver, retrocede un nivel y ajusta alturas.
348 -->
349 function onBackButtonClick(event) {
350 const target = event.target.closest('.navbar-mobile .back-btn .back');
351 if (!target) {
352 return;
353 }
354
355 <#-- Evita la navegacion por defecto si es el boton de volver -->
356 event.preventDefault();
357 event.stopPropagation();
358
359 if (!event.target.closest('.main-navigation-mobile-wrapper')) {
360 return;
361 }
362
363 <#-- Obtiene el nivel del submenu actual -->
364 const level = getLevelFromElement(target);
365 if (!level) {
366 return;
367 }
368 handleBackClick(event, level);
369 }
370
371 <#--
372 FUNCION: onHamburgerClick
373 DESCRIPCION: Abre/cierra el menu, inserta HTML, clona extras y marca item activo.
374 -->
375 function onHamburgerClick(event) {
376 const hamburgerButton = event.target.closest('.hamburger-button-mobile');
377 if (!hamburgerButton) {
378 return;
379 }
380
381 <#-- Inserta el html del menu de navegacion con todos sus niveles -->
382 const navbarMobile = document.querySelector('.navbar-mobile');
383 if (navbarMobile) {
384 navbarMobile.innerHTML = window.__mainNavigationMobileHTML || '';
385 }
386
387 const navWrapper = document.querySelector('.main-navigation-mobile-wrapper');
388 const faBars = hamburgerButton.querySelector('.fa-bars');
389 const faXmark = hamburgerButton.querySelector('.fa-xmark');
390 const isActive = hamburgerButton.classList.contains('active');
391 const privateArea = document.querySelector('.private-area-wrapper');
392 const navbarListMenu = navbarMobile ? navbarMobile.querySelector('.list-menu') : null;
393 const logoDer = document.querySelector('.portal-parent-brand');
394 const nameParent = document.querySelector('.portal-parent-name');
395 const languageSelector = document.querySelector('.language-wrapper .language-selector');
396 const overlay = document.getElementById('overlay');
397 const level1 = navbarMobile ? navbarMobile.querySelector('ul.level-1') : null;
398 const allUls = navbarMobile ? navbarMobile.querySelectorAll('ul') : [];
399
400 setHamburgerState(!isActive, hamburgerButton, navWrapper, faBars, faXmark);
401
402 setOverlay(!isActive, overlay);
403
404 <#-- Ajusta el alto del submenu -->
405 if (!isActive) {
406 if (level1) {
407 level1.style.height = level1.offsetHeight + 'px';
408 }
409 }
410
411 <#-- Anade el area privada y el selector de idiomas al menu movil -->
412 addMenuExtras(navbarListMenu, logoDer, nameParent, privateArea, languageSelector);
413
414 setActiveMenuItemFromBreadcrumb();
415 <#-- Se encarga de que al abrir el menu se pinte el nivel actual de navegacion -->
416 let deepestUl = null;
417 let maxDepth = 0;
418
419 allUls.forEach(function(ul) {
420 const lis = ul.querySelectorAll('li.selected.active');
421
422 if (lis.length > 0) {
423 ul.classList.add('active');
424
425 <#-- Calcular profundidad para saber si este es el mas profundo -->
426 let depth = 0;
427 let tempUl = ul;
428 while (tempUl.parentElement) {
429 tempUl = tempUl.parentElement;
430 if (tempUl.tagName === 'UL') {
431 depth++;
432 }
433 }
434
435 if (depth > maxDepth) {
436 maxDepth = depth;
437 deepestUl = ul;
438 }
439 }
440 });
441
442 <#-- Establecemos el height en el level 1 igual al offsetHeight del mas profundo -->
443 if (deepestUl && level1) {
444 level1.style.height = deepestUl.offsetHeight + 'px';
445 }
446
447 }
448
449 <#--
450 FUNCION: onDocumentClick
451 DESCRIPCION: Cierra el menu si se hace click fuera del wrapper o del boton.
452 -->
453 function onDocumentClick(event) {
454 const navWrapper = document.querySelector('.main-navigation-mobile-wrapper');
455 const hamburgerButton = document.querySelector('.hamburger-button-mobile');
456
457 if (navWrapper && !navWrapper.contains(event.target) && hamburgerButton && !hamburgerButton.contains(event.target)) {
458 closeMobileMenuDropdown();
459 }
460 }
461
462 <#--
463 FUNCION: onDocumentKeydown
464 DESCRIPCION: Cierra el menu al pulsar ESC si esta abierto.
465 -->
466 function onDocumentKeydown(event) {
467 const navWrapper = document.querySelector('.main-navigation-mobile-wrapper');
468 const hamburgerButton = document.querySelector('.hamburger-button-mobile');
469 if (event.key === 'Escape' || event.keyCode === 27) {
470 if (navWrapper && hamburgerButton && hamburgerButton.classList.contains('active')) {
471 closeMobileMenuDropdown();
472 }
473 }
474 }
475
476 <#--
477 FUNCION: bindMobileWrapperEvents
478 DESCRIPCION: Enlaza los eventos de submenu/back al wrapper para evitar listeners globales.
479 Asegura que siempre hay un solo wrapper con listeners activos, incluso cuando Liferay SPA recrea el DOM.
480 -->
481 function bindMobileWrapperEvents() {
482 const navWrapper = document.querySelector('.main-navigation-mobile-wrapper');
483 if (!navWrapper) {
484 return;
485 }
486
487 if (window.__mainNavigationMobileWrapper === navWrapper) {
488 return;
489 }
490
491 if (window.__mainNavigationMobileWrapper) {
492 window.__mainNavigationMobileWrapper.removeEventListener('click', onSubmenuToggleClick);
493 window.__mainNavigationMobileWrapper.removeEventListener('click', onBackButtonClick);
494 }
495
496 window.__mainNavigationMobileWrapper = navWrapper;
497 navWrapper.addEventListener('click', onSubmenuToggleClick);
498 navWrapper.addEventListener('click', onBackButtonClick);
499 }
500
501 <#--
502 FUNCION: initMainNavigationMobile
503 DESCRIPCION: Inicializa eventos globales una sola vez y re-enlaza eventos del wrapper.
504 -->
505 function initMainNavigationMobile() {
506 if (!window.__mainNavigationMobileEventsBound) {
507 window.__mainNavigationMobileEventsBound = true;
508 document.addEventListener('click', onHamburgerClick);
509 document.addEventListener('click', onDocumentClick);
510 document.addEventListener('keydown', onDocumentKeydown);
511 }
512
513 bindMobileWrapperEvents();
514 }
515
516 <#--
517 INICIALIZACION:
518 - load: Carga inicial normal cuando todo los recursos estan cargados.
519 - endNavigate: Evento especifico de Liferay SPA (Single Page Application) para reinicializar al navegar sin recargar.
520 -->
521 window.addEventListener("load", initMainNavigationMobile);
522 Liferay.on('endNavigate', initMainNavigationMobile);
523
524 })();
525
526</script>