1/*1790587750000*/ 2/** 3* Funcionalidad de menú lateral flotante. 4* Aplica comportamiento sticky al menú cuando se habilita mediante el atributo data-sticky-menu="true". 5* 6* Anotaciones: 7* - Solo funciona en pantallas >= 992px (Bootstrap lg breakpoint) 8* - Considera la altura del header fijo y del menú de control de Liferay 9* - Actualmente solo se aplica al menú lateral UM_GL_PLT_Menu_Navegacion_Lateral 10* - Para aplicarlo a otros componentes hay que revisar cómo parametrizarlo 11*/ 12 13(function() { 14 'use strict'; 15 16 /** 17 * Calcula la altura total de los elementos fijos en la parte superior 18 * Tiene en cuenta el header principal y el menú de administración de Liferay 19 */ 20 function getFixedHeaderOffset() { 21 let offset = 0; 22 23 // Suma la altura del header principal 24 const header = document.querySelector('#header'); 25 if (header) { 26 offset += header.offsetHeight; 27 } 28 29 // Suma la altura del menú de control de Liferay (si existe y está visible) 30 const controlMenu = document.querySelector('#ControlMenu'); 31 if (controlMenu) { 32 offset += controlMenu.offsetHeight; 33 } 34 35 // Añade espaciado extra para evitar solapamientos 36 offset += 15; 37 38 return offset; 39 } 40 41 42 /** 43 * Este módulo gestiona el enlace activo basado en la sección visible en pantalla 44 */ 45 class AnchorSpyManager { 46 constructor(menuElement) { 47 this.menu = menuElement; 48 this.anchorLinks = this.menu.querySelectorAll('[data-anchor]'); 49 this.observerOptions = { 50 root: null, 51 rootMargin: '-50% 0px -50% 0px', // Se activa cuando el elemento está en el centro del viewport 52 threshold: 0 53 }; 54 55 if (this.anchorLinks.length === 0) { 56 return; 57 } 58 59 this.init(); 60 } 61 62 /** 63 * Inicializa el observador de secciones 64 */ 65 init() { 66 // Crea un mapa de anchor -> elemento de menú 67 this.anchorMap = new Map(); 68 this.anchorLinks.forEach(link => { 69 const anchorId = link.getAttribute('data-anchor'); 70 this.anchorMap.set(anchorId, link); 71 }); 72 73 // Crea el IntersectionObserver para detectar secciones visibles 74 this.observer = new IntersectionObserver((entries) => { 75 this.handleIntersection(entries); 76 }, this.observerOptions); 77 78 // Observa todos los elementos con id que coinciden con los anchors 79 this.anchorMap.forEach((link, anchorId) => { 80 const targetElement = document.getElementById(anchorId); 81 if (targetElement) { 82 this.observer.observe(targetElement); 83 } 84 }); 85 86 // Fallback: inicializa con hash actual si existe 87 this.updateFromHash(); 88 89 // Escucha cambios de hash 90 window.addEventListener('hashchange', () => this.updateFromHash()); 91 } 92 93 /** 94 * Gestiona los cambios de intersección 95 */ 96 handleIntersection(entries) { 97 entries.forEach(entry => { 98 if (entry.isIntersecting) { 99 const anchorId = entry.target.id; 100 this.setActiveAnchor(anchorId); 101 } 102 }); 103 } 104 105 /** 106 * Actualiza el elemento activo basándose en el hash de la URL 107 */ 108 updateFromHash() { 109 const hash = window.location.hash.substring(1); 110 if (hash && this.anchorMap.has(hash)) { 111 this.setActiveAnchor(hash); 112 } else if (!hash && this.anchorMap.size > 0) { 113 // Si no hay hash, limpia los activos 114 this.clearAllActive(); 115 } 116 } 117 118 /** 119 * Marca como activo el enlace correspondiente a un anchor 120 */ 121 setActiveAnchor(anchorId) { 122 // Limpia todos los activos 123 this.clearAllActive(); 124 125 // Marca el nuevo activo 126 const activeLink = this.anchorMap.get(anchorId); 127 if (activeLink) { 128 activeLink.classList.add('um-menu-lateral__link--active'); 129 } 130 } 131 132 /** 133 * Limpia todos los elementos activos del menú 134 */ 135 clearAllActive() { 136 this.anchorLinks.forEach(link => { 137 link.classList.remove('um-menu-lateral__link--active'); 138 }); 139 } 140 141 /** 142 * Destruye el observador cuando sea necesario 143 */ 144 destroy() { 145 if (this.observer) { 146 this.observer.disconnect(); 147 } 148 window.removeEventListener('hashchange', () => this.updateFromHash()); 149 } 150 } 151 152 class StickyMenuManager { 153 constructor(menuElement) { 154 this.menu = menuElement; 155 this.wrapper = menuElement.closest('.um-menu-lateral-wrapper'); 156 this.container = this.menu.closest('.portlet-column') || this.menu.closest('[data-floating="true"]')?.parentElement; 157 158 this.isFloating = false;
159 this.isSupported = this.checkSupport(); 160 this.originalWidth = null; 161 this.originalLeft = null; 162 163 // Inicializa el controlador si es necesario 164 this.anchorSpy = null; 165 if (this.menu.hasAttribute('data-anchor-spy')) { 166 this.anchorSpy = new AnchorSpyManager(this.menu); 167 } 168 169 if (!this.isSupported || !this.container) { 170 return; 171 } 172 173 174 this.init(); 175 } 176 177 178 /** 179 * Comprueba si el navegador soporta las propiedades necesarias 180 */ 181 checkSupport() { 182 return typeof window.innerWidth !== 'undefined'; 183 } 184 185 186 /** 187 * Inicializa los listeners y calcula dimensiones iniciales 188 */ 189 init() { 190 this.updateDimensions(); 191 192 window.addEventListener('scroll', () => this.handleScroll(), { passive: true }); 193 window.addEventListener('resize', () => this.handleResize(), { passive: true }); 194 195 196 // Calcula dimensiones nuevamente después de que el DOM esté completamente renderizado 197 setTimeout(() => this.updateDimensions(), 100); 198 } 199 200 201 /** 202 * Actualiza las dimensiones y posiciones del contenedor y menú 203 */ 204 updateDimensions() { 205 if (!this.container) return; 206 207 208 const containerRect = this.container.getBoundingClientRect(); 209 const referenceElement = this.wrapper || this.menu; 210 const referenceRect = referenceElement.getBoundingClientRect(); 211 212 213 // Posición del contenedor respecto al documento 214 this.containerTop = window.scrollY + containerRect.top; 215 this.containerBottom = this.containerTop + containerRect.height; 216 this.containerHeight = containerRect.height; 217 this.containerLeft = containerRect.left; 218 219 220 // Dimensiones del menú 221 this.menuHeight = this.menu.offsetHeight; 222 // Captura el ancho original del menú antes de que sea flotante 223 if (this.originalWidth === null || this.originalLeft === null) { 224 // Ajusta la posición necesaria respetando paddings/gutters del layout 225 this.originalWidth = referenceRect.width; 226 this.originalLeft = referenceRect.left; 227 } 228 229 // Altura del viewport 230 this.viewportHeight = window.innerHeight; 231 } 232 233 /** 234 * Gestiona el evento de scroll 235 */ 236 handleScroll() { 237 // Solo aplica en pantallas >= 992px 238 if (window.innerWidth < 992) { 239 this.disableFloating(); 240 return; 241 } 242 243 const scrollY = window.scrollY; 244 245 // Determina si el contenedor ha pasado el top del viewport 246 const containerTopInViewport = this.containerTop - scrollY; 247 const containerBottomInViewport = this.containerBottom - scrollY; 248 249 // Obtiene el offset del header fijo 250 const fixedHeaderOffset = getFixedHeaderOffset(); 251 252 // El menú debe flotar si: 253 // 1. El contenedor ha pasado el top del viewport (containerTopInViewport <= fixedHeaderOffset) 254 // 2. El menú cabe completamente en el viewport 255 // 3. El final del menú no supera el final del contenedor 256 const shouldFloat = 257 containerTopInViewport <= fixedHeaderOffset && 258 this.menuHeight <= this.viewportHeight && 259 this.menuHeight <= containerBottomInViewport; 260 261 if (shouldFloat && !this.isFloating) { 262 this.enableFloating(); 263 } else if (!shouldFloat && this.isFloating) { 264 this.disableFloating(); 265 } 266 267 // Si está flotando, verifica si necesita limitarse por el bottom del contenedor 268 if (this.isFloating) { 269 this.updateFloatingPosition(containerBottomInViewport, fixedHeaderOffset); 270 } 271 } 272 273 /** 274 * Actualiza la posición del menú flotante 275 * Considera el offset del header fijo superior y mantiene el ancho original 276 */ 277 updateFloatingPosition(containerBottomInViewport, fixedHeaderOffset) { 278 // Si el final del contenedor entra en el viewport, el menú se mueve con él 279 const menuBottom = this.menuHeight + fixedHeaderOffset; 280 281 if (containerBottomInViewport < menuBottom) { 282 // El contenedor está a punto de salir por abajo, sube el menú 283 const offset = containerBottomInViewport - this.menuHeight; 284 this.menu.style.top = offset + 'px'; 285 } else { 286 // El contenedor está completamente visible, el menú se posiciona bajo el header fijo 287 this.menu.style.top = fixedHeaderOffset + 'px'; 288 } 289 290 // Mantiene el ancho y posición horizontal original 291 this.menu.style.left = this.originalLeft + 'px'; 292 this.menu.style.width = this.originalWidth + 'px'; 293 } 294 295 /** 296 * Habilita el comportamiento flotante del menú 297 */ 298 enableFloating() { 299 if (!this.isFloating) { 300 this.menu.classList.add('um-menu-lateral--floating'); 301 this.isFloating = true; 302 } 303 } 304 305 /** 306 * Deshabilita el comportamiento flotante del menú 307 */ 308 disableFloating() { 309 if (this.isFloating) { 310 this.menu.classList.remove('um-menu-lateral--floating'); 311 312 // Limpia todos los estilos inline 313 this.menu.style.top = ''; 314 this.menu.style.left = ''; 315 this.menu.style.width = ''; 316 317 this.isFloating = false;
318 } 319 } 320 321 /** 322 * Gestiona el evento de resize de la ventana 323 */ 324 handleResize() { 325 // Si cambia a pantalla pequeña, deshabilita 326 if (window.innerWidth < 992) { 327 this.disableFloating(); 328 return; 329 } 330 331 // Recalcula el ancho original en caso de que cambie el tamaño de la ventana 332 this.originalWidth = null; 333 this.originalLeft = null; 334 335 // Actualiza dimensiones y recalcula 336 this.updateDimensions(); 337 this.handleScroll(); 338 } 339 } 340 341 // Función para actualizar el valor de la variable CSS --header-height 342 function updateHeaderHeightVar() { 343 var header = document.querySelector('#header'); 344 var controlMenu = document.querySelector('.cadmin.control-menu-container'); 345 var headerHeight = header ? header.offsetHeight : 0; 346 var controlMenuHeight = controlMenu ? controlMenu.offsetHeight : 0; 347 348 var totalHeight = headerHeight + controlMenuHeight + 20; // +20px de margen 349 350 // Asigna la variable CSS en :root 351 document.documentElement.style.setProperty('--header-height', totalHeight + 'px'); 352 } 353 354 /** 355 * Inicializa todos los menús flotantes cuando el DOM está listo 356 */ 357 function initStickyMenus() { 358 const stickyMenus = document.querySelectorAll('[data-sticky-menu="true"]'); 359 360 stickyMenus.forEach(menu => { 361 new StickyMenuManager(menu); 362 }); 363 364 // Inicializa la variable CSS --header-height para scroll de anclas 365 updateHeaderHeightVar(); 366 window.addEventListener('resize', updateHeaderHeightVar); 367 } 368 369 // Espera a que el DOM esté completamente cargado 370 if (document.readyState === 'loading') { 371 document.addEventListener('DOMContentLoaded', initStickyMenus); 372 } else { 373 initStickyMenus(); 374 } 375})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.