PageSourceSearch

https://estudis.uib.es/o/uib-theme/js/main.js?browserId=chrome&min…=js&languageId=es_ES&t=1790650608000

js uib.es collected 2026-10-02 09:50:29 UTC 21,541 bytes, 654 lines download raw bytes

1/*1790650608000*/
2/*==========================================================
3 *  Tema Liferay UIB — script principal
4 *  Refactor: ES6+, funcions curtes (SRP), DRY, accessible i segur.
5 *  Es manté l'API pública global perquè el tema/fragments hi
6 *  segueixen accedint des d'atributs inline (onclick, etc.).
7 *========================================================*/
8(function (window, document) {
9	'use strict';
10
11	/*------------------------------------------------------
12	 *  Configuració centralitzada
13	 *----------------------------------------------------*/
14	const CONFIG = {
15		cookieMidaText: 'nde-textsize',
16		midaTextPerDefecte: 16,
17		midaTextMinima: 10,
18		midaTextMaxima: 24,
19		incrementMidaText: 2,
20		diesCaducitatCookie: 365,
21		duracioScrollMs: 200,
22		intentsEsperaTabs: 30,
23		// Barres fixes superiors per descomptar en el scroll a pestanya
24		selectorsCapcaleraFixa: ['.control-menu', '.cadmin-control-menu'],
25		alcadaCapcaleraPerDefecte: 32,
26		// Menú d'eines responsive: breakpoint mòbil de Clay (media-breakpoint-down(md))
27		consultaMobil: '(max-width: 991.98px)',
28		idMenuEines: 'navigation_tools',
29		idMenuEinesMobil: 'navigation_tools_mobile',
30		idBotoMenuGeneral: 'menu-tools-general-button',
31		idMenuGeneral: 'menu-tools-general',
32		// Agrupador únic del menú d'eines, desplegat al panell mòbil
33		classeAgrupadorDesplegat: 'uib-agrupador-desplegat',
34		selectorArrelMenu: 'ul[role="menubar"], ul.navbar-nav'
35	};
36
37	// Només IDs vàlids (evita injecció via location.hash)
38	const PATRO_ID_VALID = /^[A-Za-z][\w-]*$/;
39
40	/*======================================================
41	 *  Mòdul: Cookies
42	 *====================================================*/
43
44	/**
45	 * Crea o actualitza una cookie amb atributs de seguretat.
46	 * @param {string} nom Nom de la cookie.
47	 * @param {string} valor Valor a emmagatzemar.
48	 * @param {number} [dies] Dies fins a la caducitat.
49	 */
50	function ndeCreateCookie(nom, valor, dies) {
51		let cookie = encodeURIComponent(nom) + '=' + encodeURIComponent(valor) + ';';
52
53		if (dies) {
54			const data = new Date();
55			data.setTime(data.getTime() + (dies * 24 * 60 * 60 * 1000));
56			cookie += ' expires=' + data.toUTCString() + ';';
57		}
58
59		cookie += ' path=/; SameSite=Lax';
60		if (window.location.protocol === 'https:') {
61			cookie += '; Secure';
62		}
63
64		document.cookie = cookie;
65	}
66
67	/**
68	 * Llegeix el valor d'una cookie pel seu nom.
69	 * @param {string} nom Nom de la cookie a cercar.
70	 * @returns {string|null} Valor de la cookie o null si no existeix.
71	 */
72	function ndeReadCookie(nom) {
73		const nomEQ = encodeURIComponent(nom) + '=';
74		const cookies = document.cookie.split(';');
75
76		for (let i = 0; i < cookies.length; i++) {
77			const c = cookies[i].trim();
78			if (c.indexOf(nomEQ) === 0) {
79				return decodeURIComponent(c.substring(nomEQ.length));
80			}
81		}
82		return null;
83	}
84
85	/*======================================================
86	 *  Mòdul: Mida del text (accessibilitat)
87	 *====================================================*/
88
89	/**
90	 * Retorna la mida de font actual del document en píxels.
91	 * @returns {number} Mida en píxels, o 0 si no es pot determinar.
92	 */
93	function ndeGetDocTextSize() {
94		const body = document.body;
95		if (!body) {
96			return 0;
97		}
98		const estil = getComputedStyle(body).fontSize || body.style.fontSize;
99		const mida = parseInt(estil, 10);
100		return isNaN(mida) ? 0 : mida;
101	}
102
103	/**
104	 * Ajusta la mida del text del document i la persisteix en cookie.
105	 * @param {string}
105 [canvi] Acció: 'incr', 'decr', 'reset' o buit (carrega cookie).
106	 * @param {number} [midaReset] Mida a aplicar quan canvi és 'reset'.
107	 */
108	function ndeSetTextSize(canvi, midaReset) {
109		const midaInicial = ndeGetDocTextSize() || CONFIG.midaTextPerDefecte;
110		const novaMida = limita(
111			calculaNovaMida(canvi, midaInicial, midaReset),
112			CONFIG.midaTextMinima,
113			CONFIG.midaTextMaxima
114		);
115
116		aplicaMidaText(novaMida);
117		ndeCreateCookie(CONFIG.cookieMidaText, novaMida + 'px', CONFIG.diesCaducitatCookie);
118	}
119
120	/**
121	 * Calcula la nova mida de font segons l'acció sol·licitada.
122	 * @param {string} canvi Acció a aplicar.
123	 * @param {number} midaInicial Mida de partida.
124	 * @param {number} midaReset Mida per a l'acció 'reset'.
125	 * @returns {number} Nova mida calculada (sense limitar).
126	 */
127	function calculaNovaMida(canvi, midaInicial, midaReset) {
128		switch (canvi) {
129			case 'incr':
130				return midaInicial + CONFIG.incrementMidaText;
131			case 'decr':
132				return midaInicial - CONFIG.incrementMidaText;
133			case 'reset':
134				return midaReset || CONFIG.midaTextPerDefecte;
135			default:
136				return llegeixMidaGuardada() || midaInicial;
137		}
138	}
139
140	function llegeixMidaGuardada() {
141		const valor = parseInt(ndeReadCookie(CONFIG.cookieMidaText), 10);
142		return isNaN(valor) ? 0 : valor;
143	}
144
145	function aplicaMidaText(midaPx) {
146		const valor = midaPx + 'px';
147		document.documentElement.style.fontSize = valor;
148		if (document.body) {
149			document.body.style.fontSize = valor;
150		}
151	}
152
153	function limita(valor, minim, maxim) {
154		return Math.min(Math.max(valor, minim), maxim);
155	}
156
157	/*======================================================
158	 *  Mòdul: Utilitats DOM
159	 *====================================================*/
160
161	/**
162	 * Indica si un element té una classe CSS.
163	 * @param {Element} element Element a comprovar.
164	 * @param {string} className Nom de la classe.
165	 * @returns {boolean} Cert si l'element conté la classe.
166	 */
167	function hasClass(element, className) {
168		return !!element && element.classList.contains(className);
169	}
170
171	/**
172	 * Ajusta la variable CSS --scroll-bar segons l'amplada de la barra de scroll.
173	 */
174	function handleWindowScrollBar() {
175		const body = document.body;
176		if (!body) {
177			return;
178		}
179		const ampladaScroll = window.innerWidth - body.clientWidth;	
180		if (ampladaScroll > 5) {
181			body.classList.add('has-scrollbar');
182			document.documentElement.style.setProperty('--scroll-bar-width', ampladaScroll + 'px');
183		} else {
184			body.classList.remove('has-scrollbar');
185		}
186	}
187
188	/**
189	 * Obre o tanca un desplegable i sincronitza l'estat ARIA.
190	 * @param {Event} event Esdeveniment que dispara l'acció.
191	 * @param {Element} item Element disparador amb l'atribut data-target.
192	 */
193	function handleDropdown(event, item) {
194		event.preventDefault();
195
196		const dropdown = document.querySelector(item.getAttribute('data-target'));
197		if (!dropdown) {
198			return;
199		}
200
201		const expandit = item.getAttribute('aria-expanded') !== 'true';
202		item.setAttribute('aria-expanded', expandit);
203		dropdown.setAttribute('aria-expanded', expandit);
204		dropdown.classList.toggle('show');
205	}
206
207	/**
208	 * Vincula activació per clic i per teclat (Espai/Enter) a un element una sola
209	 * vegada (idempotent: es pot tornar a cridar després d'una navegació SPA).
210	 * @param {Element} element Element a fer accessible.
211	 * @param {Function} gestor Gestor a executar en el clic.
212	 */
213	function afegeixActivacioAccessible(element, gestor) {
214		if (!element || element.hasAttribute('data-uib-activacio')) {
215			return;
216		}
217		element.setAttribute('data-uib-activacio', 'true');
218		element.addEventListener('click', gestor);
219		element.addEventListener('keydown', function (event) {
220			if (event.code === 'Space' || event.code === 'Enter') {
221				event.preventDefault();
222				element.click();
223			}
224		});
225	}
226
227	/*======================================================
228	 *  Mòdul: Scroll animat
229	 *====================================================*/
230
231	/**
232	 * Desplaça la finestra fins a una posició amb animació suau.
233	 * @param {number} desti Posició vertical de destí (px).
234	 * @param {number} duracio Durada de l'animació (ms).
235	 */
236	function scrollToPosition(desti, duracio) {
237		const inici = window.scrollY || document.documentElement.scrollTop;
238		const canvi = desti - inici;
239		const tempsInici = performance.now();
240
241		function easeInOutQuad(t) {
242			return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
243		}
244
245		function anima(tempsActual) {
246			const transcorregut = tempsActual - tempsInici;
247			const progres = Math.min(transcorregut / duracio, 1);
248			window.scrollTo(0, inici + canvi * easeInOutQuad(progres));
249
250			if (transcorregut < duracio) {
251				requestAnimationFrame(anima);
252			}
253		}
254
255		requestAnimationFrame(anima);
256	}
257
258	/**
259	 * Ajusta la posició objectiu descomptant l'alçada de les barres fixes superiors.
260	 * @param {number} topPixels Posició vertical de l'element objectiu.
261	 * @returns {number} Posició de scroll ajustada (mai negativa).
262	 */
263	function calculaHeader(topPixels) {
264		return Math.max(topPixels - calculaAlcadaCapcalera(), 0);
265	}
266
267	function calculaAlcadaCapcalera() {
268		let alcada = 0;
269		CONFIG.selectorsCapcaleraFixa.forEach(function (selector) {
270			const element = document.querySelector(selector);
271			if (element) {
272				alcada += element.getBoundingClientRect().height;
273			}
274		});
275		return alcada || CONFIG.alcadaCapcaleraPerDefecte;
276	}
277
278	/*======================================================
279	 *  Mòdul: Pestanyes des del hash (Liferay + UIB)
280	 *====================================================*/
281
282	/**
283	 * Obre la pestanya indicada al hash de la URL i hi desplaça la vista.
284	 */
285	function obrePestanyaSegonsHash() {
286		const hash = document.location.hash;
287		if (!hash || hash.indexOf('/') >= 0) {
288			return;
289		}
290
291		const idPestanya = hash.replace(/^#/, '');
292		if (!PATRO_ID_VALID.test(idPestanya)) {
293			return;
294		}
295
296		const capcalera = document.getElementById(idPestanya);
297		if (!capcalera) {
298			return;
299		}
300
301		capcalera.click();
302		const topPixels = capcalera.getBoundingClientRect().top + window.scrollY;
303		scrollToPosition(calculaHeader(topPixels), CONFIG.duracioScrollMs);
304	}
305
306	/**
307	 * Espera que el component de pestanyes existeixi abans d'executar el callback.
308	 * Reintents limitats per frame per evitar bucles infinits.
309	 * @param {Function} callback Acció a executar quan el component estigui llest.
310	 * @param {number} intentsRestants Nombre màxim de reintents.
311	 */
312	function esperaComponentTabs(callback, intentsRestants) {
313		if (document.querySelector('.component-tabs')) {
314			callback();
315		} else if (intentsRestants > 0) {
316			requestAnimationFrame(function () {
317				esperaComponentTabs(callback, intentsRestants - 1);
318			});
319		}
320	}
321
322	/*======================================================
323	 *  Mòdul: Menú d'eines responsive (un sol portlet)
324	 *  A escriptori el menú viu a #navigation_tools (barra superior);
325	 *  a mòbil es trasllada a #navigation_tools_mobile, dins el panell
326	 *  de l'hamburguesa. Es mou (no es copia) per no duplicar IDs ni
327	 *  perdre els esdeveniments dels desplegables del portlet.
328	 *====================================================*/
329
330	/**
331	 * Indica si la pantalla és de mida mòbil (per sota del breakpoint lg de Clay).
332	 * @returns {boolean} Cert en mode mòbil.
333	 */
334	function esModeMobil() {
335		return window.matchMedia(CONFIG.consultaMobil).matches;
336	}
337
338	/**
339	 * Trasllada tots els nodes fills d'un contenidor a un altre.
340	 * @param {Element} origen Contenidor d'origen.
341	 * @param {Element} desti Contenidor de destí.
342	 */
343	function mouFills(origen, desti) {
344		while (origen.firstChild) {
345			desti.appendChild(origen.firstChild);
346		}
347	}
348
349	/**
350	 * Col·loca el menú d'eines al contenidor que correspon a la mida de pantalla.
351	 */
352	function reubicaMenuEines() {
353		const escriptori = document.getElementById(CONFIG.idMenuEines);
354		const mobil = document.getElementById(CONFIG.idMenuEinesMobil);
355		if (!escriptori || !mobil) {
356			return;
357		}
358		if (esModeMobil()) {
359			mouFills(escriptori, mobil);
360		} else {
361			mouFills(mobil, escriptori);
362		}
363	}
364
365	/**
366	 * Tanca el panell del menú general (hamburguesa) i sincronitza l'estat ARIA.
367	 */
368	function tancaMenuGeneral() {
369		const boto = document.getElementById(CONFIG.idBotoMenuGeneral);
370		const menu = document.getElementById(CONFIG.idMenuGeneral);
371		if (!boto || !menu || !menu.classList.contains('show')) {
372			return;
373		}
374		boto.setAttribute('aria-expanded', 'false');
375		menu.setAttribute('aria-expanded', 'false');
376		menu.classList.remove('show');
377	}
378
379	/**
380	 * Tanca el panell obert amb la tecla Escape i torna el focus a l'hamburguesa.
381	 * @param {KeyboardEvent} event Esdeveniment de teclat.
382	 */
383	function tancaMenuGeneralAmbEscape(event) {
384		const menu = document.getElementById(CONFIG.idMenuGeneral);
385		if (event.key !== 'Escape' || !menu || !menu.classList.contains('show')) {
386			return;
387		}
388		tancaMenuGeneral();
389		const boto = document.getElementById(CONFIG.idBotoMenuGeneral);
390		if (boto) {
391			boto.focus();
392		}
393	}
394
395	/**
396	 * En creuar el breakpoint mòbil: tanca el panell i recol·loca el menú d'eines.
397	 */
398	function enCanviarModePantalla() {
399		tancaMenuGeneral();
400		actualitzaMenuEines();
401	}
402
403	/**
404	 * Inicialitza el menú d'eines responsive i els seus escoltadors.
405	 */
406	function inicialitzaMenuEines() {
407		actualitzaMenuEines();
408		const consulta = window.matchMedia(CONFIG.consultaMobil);
409		if (typeof consulta.addEventListener === 'function') {
410			consulta.addEventListener('change', enCanviarModePantalla);
411		} else {
412			consulta.addListener(enCanviarModePantalla);
413		}
414		document.addEventListener('keydown', tancaMenuGeneralAmbEscape);
415	}
416
417	/**
418	 * Executa el callback quan el DOM és a punt (o immediatament si ja ho és).
419	 * @param {Function} callback Funció a executar.
420	 */
421	function quanDomPreparat(callback) {
422		if (document.readyState === 'loading') {
423			document.addEventListener('DOMContentLoaded', callback);
424		} else {
425			callback();
426		}
427	}
428
429	/*======================================================
430	 *  Mòdul: Agrupador únic desplegat (menú d'eines, panell mòbil)
431	 *  Si el menú d'eines té una sola opció de primer nivell i és un
432	 *  agrupador (submenú sense enllaç propi), al panell mòbil es mostra
433	 *  sempre desplegada. Es manté oberta encara que la interacció de
434	 *  navegació de Liferay tregui la classe 'open' en perdre el hover.
435	 *  A escriptori conserva el desplegable normal (hover/focus).
436	 *====================================================*/
437
438	const observadorsAgrupador = new WeakMap();
439
440	/**
441	 * Retorna el primer fill directe d'un element amb l'etiqueta indicada.
442	 * @param {Element} element Element pare.
443	 * @param {string} etiqueta Nom de l'etiqueta en majúscules (p. ex. 'UL').
444	 * @returns {Element|null} Fill trobat o null.
445	 */
446	function fillDirecte(element, etiqueta) {
447		return Array.prototype.find.call(element.children, function (fill) {
448			return fill.tagName === etiqueta;
449		}) || null;
450	}
451
452	/**
453	 * Indica si l'enllaç d'una opció de menú porta a alguna pàgina.
454	 * @param {Element} enllac Enllaç de l'opció.
455	 * @returns {boolean} Cert si té una destinació navegable.
456	 */
457	function teDestiNavegable(enllac) {
458		const desti = (enllac.getAttribute('href') || '').trim();
459		return desti !== '' && desti !== '#' && desti.toLowerCase().indexOf('javascript:') !== 0;
460	}
461
462	/**
463	 * Indica si una opció de menú és un agrupador: té submenú i no té enllaç propi.
464	 * @param {Element} opcio Opció de menú (li).
465	 * @returns {boolean} Cert si és un agrupador.
466	 */
467	function esAgrupador(opcio) {
468		const enllac = fillDirecte(opcio, 'A');
469		return !!enllac && !!fillDirecte(opcio, 'UL') && !teDestiNavegable(enllac);
470	}
471
472	/**
473	 * Retorna l'única opció de primer nivell del menú d'un contenidor si és un agrupador.
474	 * @param {Element} contenidor Contenidor del menú d'eines.
475	 * @returns {Element|null} Agrupador únic, o null si no se'n compleixen les condicions.
476	 */
477	function cercaAgrupadorUnic(contenidor) {
478		const arrel = contenidor.querySelector(CONFIG.selectorArrelMenu);
479		if (!arrel) {
480			return null;
481		}
482		const opcions = Array.prototype.filter.call(arrel.children, function (fill) {
483			return fill.tagName === 'LI';
484		});
485		return opcions.length === 1 && esAgrupador(opcions[0]) ? opcions[0] : null;
486	}
487
488	/**
489	 * Aplica l'estat obert o tancat a un agrupador (classe 'open' i atributs ARIA).
490	 * @param {Element} agrupador Agrupador (li).
491	 * @param {boolean} obert Estat a aplicar.
492	 */
493	function marcaEstatAgrupador(agrupador, obert) {
494		agrupador.classList.toggle('open', obert);
495		[fillDirecte(agrupador, 'A'), fillDirecte(agrupador, 'UL')].forEac
495h(function (node) {
496			if (node) {
497				node.setAttribute('aria-expanded', String(obert));
498			}
499		});
500	}
501
502	/**
503	 * Desplega l'agrupador de manera permanent (idempotent).
504	 * @param {Element} agrupador Agrupador (li).
505	 */
506	function despleguaAgrupador(agrupador) {
507		if (observadorsAgrupador.has(agrupador)) {
508			return;
509		}
510		agrupador.classList.add(CONFIG.classeAgrupadorDesplegat);
511		marcaEstatAgrupador(agrupador, true);
512		const observador = new MutationObserver(function () {
513			if (!agrupador.classList.contains('open')) {
514				agrupador.classList.add('open');
515			}
516		});
517		observador.observe(agrupador, { attributes: true, attributeFilter: ['class'] });
518		observadorsAgrupador.set(agrupador, observador);
519	}
520
521	/**
522	 * Torna l'agrupador al comportament normal de desplegable (hover/focus).
523	 * @param {Element} agrupador Agrupador (li).
524	 */
525	function plegaAgrupador(agrupador) {
526		const observador = observadorsAgrupador.get(agrupador);
527		if (!observador) {
528			return;
529		}
530		observador.disconnect();
531		observadorsAgrupador.delete(agrupador);
532		agrupador.classList.remove(CONFIG.classeAgrupadorDesplegat);
533		marcaEstatAgrupador(agrupador, false);
534	}
535
536	/**
537	 * Desplega l'agrupador únic quan és al panell mòbil i el plega a la barra d'escriptori.
538	 */
539	function sincronitzaAgrupadorUnic() {
540		const mobil = document.getElementById(CONFIG.idMenuEinesMobil);
541		const escriptori = document.getElementById(CONFIG.idMenuEines);
542		const agrupadorMobil = mobil && cercaAgrupadorUnic(mobil);
543		const agrupadorEscriptori = escriptori && cercaAgrupadorUnic(escriptori);
544		if (agrupadorMobil) {
545			despleguaAgrupador(agrupadorMobil);
546		}
547		if (agrupadorEscriptori) {
548			plegaAgrupador(agrupadorEscriptori);
549		}
550	}
551
552	/**
553	 * Col·loca el menú d'eines segons la mida de pantalla i hi ajusta l'agrupador únic.
554	 */
555	function actualitzaMenuEines() {
556		reubicaMenuEines();
557		sincronitzaAgrupadorUnic();
558	}
559
560	/*======================================================
561	 *  Inicialització
562	 *====================================================*/
563
564	/** Punt d'entrada incrustat al document (compatibilitat amb el tema). */
565	function init() {
566		// Reservat per a inicialitzacions inline del tema.
567	}
568	
569	
570	
571	
572	/**
573	 * Obre la pestanya del hash quan els fragments ja han registrat els seus esdeveniments.
574	 */
575	function inicialitzaPestanyesHash() {
576		// Els scripts dels fragments són type="module" (diferits): s'executen abans de DOMContentLoaded
577		quanDomPreparat(function () {
578			esperaComponentTabs(obrePestanyaSegonsHash, CONFIG.intentsEsperaTabs);
579		});
580		window.addEventListener('hashchange', obrePestanyaSegonsHash);
581	}
582
583	/**
584	 * Registra els esdeveniments de Liferay necessaris per al tema.
585	 */
586	function inicialitzaLiferay() {
587		if (typeof window.Liferay === 'undefined') {
588			return;
589		}
590		// Navegació SPA (SennaJS): es repinten capçalera i fragments
591		window.Liferay.on('endNavigate', function () {
592			inicialitzaMenus();
593			actualitzaMenuEines();
594			requestAnimationFrame(function () {
595				esperaComponentTabs(obrePestanyaSegonsHash, CONFIG.intentsEsperaTabs);
596			});
597		});
598	}
599	
600	
601
602
603
604	function inicialitzaMenus() {
605		const selectorsToggler = [
606			'#menu-tools-general-button',
607			'#menu-site-button',
608			'#btn-header-search',
609			'#btn-header-font'
610		];
611
612		selectorsToggler.forEach(function (selector) {
613			const boto = document.querySelector(selector);
614			afegeixActivacioAccessible(boto, function (event) {
615				handleDropdown(event, this);
616			});
617		});
618
619		const botoTancarCerca = document.getElementById('btn-close-search');
620		afegeixActivacioAccessible(botoTancarCerca, function () {
621			const cerca = document.getElementById('searchTools');
622			if (cerca) {
623				cerca.classList.remove('show');
624			}
625		});
626	}
627
628	/*======================================================
629	 *  Exposició de l'API pública (handlers inline del tema)
630	 *====================================================*/
631	window.init = init;
632	window.ndeSetTextSize = ndeSetTextSize;
633	window.ndeGetDocTextSize = ndeGetDocTextSize;
634	window.ndeCreateCookie = ndeCreateCookie;
635	window.ndeReadCookie = ndeReadCookie;
636	window.hasClass = hasClass;
637	window.handleWindowScrollBar = handleWindowScrollBar;
638	window.handleDropdown = handleDropdown;
639	window.scrollToPosition = scrollToPosition;
640	window.calculaHeader = calculaHeader;
641
642	/*======================================================
643	 *  Arrancada
644	 *====================================================*/
645	ndeSetTextSize();                 // Aplica la mida guardada a la cookie
646	inicialitzaLiferay();             // Gestió de pestanyes via hash
647	quanDomPreparat(function () {   // Menús de la capçalera i menú d'eines responsive
648		inicialitzaMenus();
649		inicialitzaMenuEines();
650	});
651	handleWindowScrollBar();
652	inicialitzaPestanyesHash();       // Gestió de pestanyes via hash
653
654})(window, document);

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.