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.