PageSourceSearch

https://www.um.es/o/um-lr-74-principal-theme/js/components/um-menu…=js&languageId=es_ES&t=1790587750000

js um.es collected 2026-10-01 08:31:36 UTC 10,766 bytes, 375 lines download raw bytes

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.