1// JS: enisa main. Compilado automaticamente desde js/modules/. 2// Compilado automaticamente. No editar directamente. 3// Regenerar con: pwsh js/build.ps1 4 5/* 6 ENISA | JS Module 7 Modulo: 01-utils 8 Descripcion: Logica del modulo. 9*/ 10document.documentElement.classList.add("js"); 11 12(function () { 13 var root = document.documentElement; 14 var keyboardIntentKeys = { 15 Tab: true, 16 Enter: true, 17 " ": true, 18 Spacebar: true, 19 ArrowUp: true, 20 ArrowRight: true, 21 ArrowDown: true, 22 ArrowLeft: true, 23 Home: true, 24 End: true, 25 PageUp: true, 26 PageDown: true, 27 Escape: true 28 }; 29 30 function hasKeyboardIntent(event) { 31 if (!event || event.altKey || event.ctrlKey || event.metaKey) return false; 32 return keyboardIntentKeys[event.key] === true; 33 } 34 35 function enableKeyboardFocus(event) { 36 if (!hasKeyboardIntent(event)) return; 37 root.classList.add("enisa-using-keyboard"); 38 } 39 40 function disableKeyboardFocus() { 41 root.classList.remove("enisa-using-keyboard"); 42 } 43 44 window.addEventListener("keydown", enableKeyboardFocus, true); 45 window.addEventListener("mousedown", disableKeyboardFocus, true); 46 window.addEventListener("pointerdown", disableKeyboardFocus, true); 47 window.addEventListener("touchstart", disableKeyboardFocus, true); 48})(); 49 50(function () { 51 var BREAKPOINTS = { 52 mobile: 768, 53 tablet: 1023.98, 54 desktop: 1279.98, 55 newsCarousel: 768 56 }; 57 58 function prefersReducedMotion() { 59 return !!( 60 window.matchMedia && 61 window.matchMedia("(prefers-reduced-motion: reduce)").matches 62 ); 63 } 64 65 function isMobileViewport(bp) { 66 var threshold = bp !== undefined ? bp : BREAKPOINTS.mobile; 67 if (window.matchMedia) return window.matchMedia("(max-width: " + threshold + "px)").matches; 68 return window.innerWidth <= threshold; 69 } 70 71 function debounce(fn, ms) { 72 var timer; 73 return function () { 74 var args = arguments; 75 var context = this; 76 77 clearTimeout(timer); 78 timer = setTimeout(function () { 79 fn.apply(context, args); 80 }, ms !== undefined ? ms : 150); 81 }; 82 } 83 84 var FOCUSABLE_SELECTOR = [ 85 "a[href]", 86 "area[href]", 87 "button:not([disabled])", 88 "input:not([disabled]):not([type='hidden'])", 89 "select:not([disabled])", 90 "textarea:not([disabled])", 91 "summary", 92 "iframe", 93 "object", 94 "embed", 95 "[contenteditable='true']", 96 "[tabindex]:not([tabindex='-1'])" 97 ].join(","); 98 99 var PROGRAMMATIC_FOCUS_SELECTOR = FOCUSABLE_SELECTOR + ",[tabindex='-1']"; 100 var supportsPreventScroll = null; 101 102 function detectPreventScrollSupport() { 103 var supported = false; 104 var probe; 105 var options; 106 107 if (supportsPreventScroll !== null) return supportsPreventScroll; 108 109 try { 110 probe = document.createElement("button"); 111 probe.type = "button"; 112 probe.style.cssText = "position:absolute;left:-9999px;top:0;width:1px;height:1px;"; 113 options = Object.defineProperty({}, "preventScroll", { 114 get: function () { 115 supported = true; 116 return true; 117 } 118 }); 119 120 document.body.appendChild(probe); 121 probe.focus(options); 122 probe.remove(); 123 } catch (error) { 124 if (probe && probe.parentNode) probe.parentNode.removeChild(probe); 125 } 126 127 supportsPreventScroll = supported; 128 return supportsPreventScroll; 129 } 130 131 function isElementVisible(element) { 132 if (!element || element.hidden) return false; 133 if (element.getAttribute("aria-hidden") === "true") return false; 134 if (element.closest("[hidden], [aria-hidden='true'], [inert]")) return false; 135 if (typeof window.getComputedStyle === "function") { 136 var style = window.getComputedStyle(element); 137 if (style.display === "none" || style.visibility === "hidden") return false; 138 } 139 if ( 140 element !== document.documentElement && 141 element !== document.body && 142 element.getClientRects && 143 element.getClientRects().length === 0 && 144 element !== document.activeElement 145 ) { 146 return false; 147 } 148 return true; 149 } 150 151 function getFocusableElements(scope, includeHidden) { 152 if (!scope || !scope.querySelectorAll) return []; 153 154 return [].slice.call(scope.querySelectorAll(FOCUSABLE_SELECTOR)).filter(function (element) { 155 var tabindex = element.getAttribute("tabindex"); 156 157 if (tabindex && parseInt(tabindex, 10) < 0) return false;
158 if (element.disabled || element.getAttribute("aria-disabled") === "true") return false; 159 if (!includeHidden && !isElementVisible(element)) return false; 160 161 return true; 162 }); 163 } 164 165 function focusWithoutScroll(element) { 166 return safeFocus(element); 167 } 168 169 function canReceiveFocus(element) { 170 if (!element || typeof element.focus !== "function") return false; 171 if (element.disabled || element.getAttribute("aria-disabled") === "true") return false; 172 if (!isElementVisible(element)) return false; 173 if (!element.matches) return false; 174 175 return element.matches(PROGRAMMATIC_FOCUS_SELECTOR); 176 } 177 178 function safeFocus(element, options) { 179 var opts = options || {}; 180 var preventScroll = opts.preventScroll !== false; 181 var scrollX = window.scrollX || window.pageXOffset || 0; 182 var scrollY = window.scrollY || window.pageYOffset || 0; 183 184 if (!canReceiveFocus(element)) return false; 185 186 try { 187 if (preventScroll && detectPreventScrollSupport()) { 188 element.focus({ preventScroll: true }); 189 } else { 190 element.focus(); 191 if (preventScroll) restoreScrollPosition(scrollY, scrollX); 192 } 193 } catch (error) { 194 element.focus(); 195 if (preventScroll) restoreScrollPosition(scrollY, scrollX); 196 } 197 198 if (preventScroll && ((window.scrollY || window.pageYOffset || 0) !== scrollY || (window.scrollX || window.pageXOffset || 0) !== scrollX)) { 199 restoreScrollPosition(scrollY, scrollX); 200 } 201 202 return true; 203 } 204 205 function focusIfActiveInside(container, fallback) { 206 if ( 207 !container || 208 !fallback || 209 typeof fallback.focus !== "function" || 210 !container.contains(document.activeElement) 211 ) { 212 return false; 213 } 214 215 return safeFocus(fallback); 216 } 217 218 function restoreScrollPosition(scrollY, scrollX) { 219 var top = Math.max(0, scrollY || 0); 220 var left = Math.max(0, scrollX || window.scrollX || window.pageXOffset || 0); 221 222 try { 223 window.scrollTo({ top: top, left: left, behavior: "auto" }); 224 } catch (error) { 225 window.scrollTo(left, top); 226 } 227 } 228 229 function setSubtreeTabbable(root, isTabbable) { 230 if (!root || !root.querySelectorAll) return; 231 232 var focusables = [].slice.call(root.querySelectorAll(FOCUSABLE_SELECTOR)); 233
234 focusables.forEach(function (element) { 235 if (isTabbable) { 236 if (element.hasAttribute("data-enisa-prev-tabindex")) { 237 var previous = element.getAttribute("data-enisa-prev-tabindex"); 238 239 if (previous === "__none__") { 240 element.removeAttribute("tabindex"); 241 } else { 242 element.setAttribute("tabindex", previous); 243 } 244 245 element.removeAttribute("data-enisa-prev-tabindex"); 246 } 247 248 return; 249 } 250 251 if (!element.hasAttribute("data-enisa-prev-tabindex")) { 252 element.setAttribute( 253 "data-enisa-prev-tabindex", 254 element.hasAttribute("tabindex") ? element.getAttribute("tabindex") : "__none__" 255 ); 256 } 257 258 element.setAttribute("tabindex", "-1"); 259 }); 260 } 261 262 function getFocusableOutside(excludedRoot) { 263 var candidates = [].slice.call(document.querySelectorAll(FOCUSABLE_SELECTOR)); 264 265 return candidates.filter(function (candidate) { 266 return ( 267 candidate && 268 candidate !== excludedRoot && 269 (!excludedRoot || !excludedRoot.contains(candidate)) && 270 canReceiveFocus(candidate) 271 ); 272 })[0] || null; 273 } 274 275 function setInert(element, isInert) { 276 if (!element) return; 277 278 if (isInert) { 279 if (element.contains(document.activeElement)) { 280 if (!safeFocus(getFocusableOutside(element)) && document.activeElement && typeof document.activeElement.blur === "function") { 281 document.activeElement.blur(); 282 } 283 } 284 285 if (!element.hasAttribute("data-enisa-prev-aria-hidden")) { 286 element.setAttribute( 287 "data-enisa-prev-aria-hidden", 288 element.hasAttribute("aria-hidden") ? element.getAttribute("aria-hidden") : "__none__" 289 ); 290 } 291 292 element.setAttribute("aria-hidden", "true"); 293 setSubtreeTabbable(element, false); 294 295 if ("inert" in element) { 296 element.inert = true; 297 } 298 299 return; 300 } 301 302 if (element.hasAttribute("data-enisa-prev-aria-hidden")) { 303 var previous = element.getAttribute("data-enisa-prev-aria-hidden"); 304 305 if (previous === "__none__") { 306 element.removeAttribute("aria-hidden"); 307 } else { 308 element.setAttribute("aria-hidden", previous); 309 } 310 311 element.removeAttribute("data-enisa-prev-aria-hidden"); 312 } 313 314 setSubtreeTabbable(element, true); 315 316 if ("inert" in element) { 317 element.inert = false; 318 } 319 } 320 321 function getHeaderOffset() { 322 var header = document.querySelector("[data-header-root], .enisa-header"); 323 var style; 324 325 if (!header) return 0; 326 327 style = window.getComputedStyle ? window.getComputedStyle(header) : null; 328 if (!style || (style.position !== "fixed" && style.position !== "sticky")) return 0; 329 330 return Math.ceil(header.getBoundingClientRect().height); 331 } 332 333 function scrollToElement(element, options) { 334 var opts = options || {}; 335 var block = opts.block || "nearest"; 336 var extraOffset = opts.offset || 16; 337 var behavior = prefersReducedMotion() ? "auto" : (opts.behavior || "smooth"); 338 var rect; 339 var headerOffset; 340 var viewportHeight; 341 var top; 342 var bottom; 343 344 if (!element || !element.getBoundingClientRect || !isElementVisible(element)) return false; 345 346 rect = element.getBoundingClientRect(); 347 headerOffset = getHeaderOffset(); 348 viewportHeight = window.innerHeight || document.documentElement.clientHeight; 349 top = rect.top - headerOffset - extraOffset; 350 bottom = rect.bottom + extraOffset; 351 352 if (top >= 0 && bottom <= viewportHeight) return false; 353 354 if (block === "nearest" && rect.top < headerOffset + extraOffset) { 355 top = Math.max(0, rect.top + window.pageYOffset - headerOffset - extraOffset); 356 } else if (block === "nearest") { 357 top = Math.max(0, rect.bottom + window.pageYOffset - viewportHeight + extraOffset); 358 } else { 359 top = Math.max(0, rect.top + window.pageYOffset - headerOffset - extraOffset); 360 } 361 362 try { 363 window.scrollTo({ top: top, left: window.pageXOffset || 0, behavior: behavior }); 364 } catch (error) { 365 window.scrollTo(window.pageXOffset || 0, top); 366 } 367 368 return true; 369 } 370 371 function createScrollCarousel(opts) { 372 opts = opts || {}; 373 374 var viewport = opts.viewport; 375 var items = opts.items || []; 376 var prevBtn = opts.prevBtn || null; 377 var nextBtn = opts.nextBtn || null; 378 var dots = opts.dots || []; 379 var dotClass = opts.activeDotClass || 'is-active'; 380 var maxVisibleDots = Math.max(1, opts.maxVisibleDots || 6); 381 var perPage = Math.max(1, opts.perPage || 1); 382 var current = 0; 383 var ticking = false;
384 var dotWindowStart = 0; 385 var DOT_INIT_ATTR = "data-enisa-carousel-dot-init"; 386 387 function clamp(n) { 388 return Math.max(0, Math.min(items.length - 1, n)); 389 } 390 391 function numPages() { 392 return Math.ceil(items.length / perPage); 393 } 394 395 function getDotPageIndex(dotIndex) { 396 return Math.floor(dotIndex / Math.max(1, perPage)); 397 } 398 399 function syncDotState(dot, active) { 400 if (!dot) return; 401 402 dot.classList.toggle(dotClass, active); 403 dot.setAttribute('aria-current', active ? 'true' : 'false'); 404 405 if (dot.getAttribute('role') === 'tab' || dot.hasAttribute('aria-selected')) { 406 dot.setAttribute('aria-selected', active ? 'true' : 'false'); 407 } 408 409 if (!dot.disabled && !dot.hidden) { 410 dot.setAttribute('tabindex', active ? '0' : '-1'); 411 } else { 412 dot.setAttribute('tabindex', '-1'); 413 } 414 } 415 416 function getVisibleDots() { 417 return dots.filter(function (dot) { 418 return dot && !dot.hidden && dot.style.display !== 'none' && !dot.disabled; 419 }); 420 } 421 422 function updateDotWindow(page) { 423 var availableDots = dots.filter(function (dot) { 424 return dot && !dot.hidden; 425 }); 426 var maxWindowStart = Math.max(0, availableDots.length - maxVisibleDots); 427 428 if (availableDots.length <= maxVisibleDots) { 429 dotWindowStart = 0; 430 } else if (page <= dotWindowStart) { 431 dotWindowStart = page; 432 } else if (page >= dotWindowStart + maxVisibleDots - 1) { 433 dotWindowStart = page; 434 } 435 436 dotWindowStart = Math.max(0, Math.min(dotWindowStart, maxWindowStart)); 437 438 var visibleStart = dotWindowStart; 439 var visibleEnd = visibleStart + maxVisibleDots; 440 var availableIndex = 0; 441 442 dots.forEach(function (dot) { 443 if (!dot) return; 444 if (dot.hidden) { 445 dot.style.display = ''; 446 return; 447 } 448 449 var isVisible = availableIndex >= visibleStart && availableIndex < visibleEnd; 450 dot.style.display = isVisible ? '' : 'none'; 451 availableIndex += 1; 452 }); 453 } 454 455 function updateState(index) { 456 current = clamp(index); 457 var page = Math.floor(current / perPage); 458 var pages = numPages(); 459 if (prevBtn) prevBtn.disabled = page === 0; 460 if (nextBtn) nextBtn.disabled = page >= pages - 1; 461 updateDotWindow(page); 462 dots.forEach(function (dot, i) { 463 syncDotState(dot, i === page); 464 }); 465 } 466 467 function goTo(index, behavior) { 468 var next = clamp(index); 469 var item = items[next]; 470 if (!item) return; 471 var offset = next === 0 ? 0 : ( 472 (window.ENISA && window.ENISA.cardsCarousel && typeof window.ENISA.cardsCarousel.getCardOffset === "function") 473 ? window.ENISA.cardsCarousel.getCardOffset(viewport, item) 474 : (function () { 475 var vRect = viewport.getBoundingClientRect(); 476 var iRect = item.getBoundingClientRect(); 477 return iRect.left - vRect.left + viewport.scrollLeft; 478 }()) 479 ); 480 481 viewport.scrollTo({ 482 left: offset, 483 top: 0, 484 behavior: behavior || (prefersReducedMotion() ? 'auto' : 'smooth') 485 }); 486 updateState(next); 487 } 488 489 function syncFromScroll() { 490 var maxScroll = viewport.scrollWidth - viewport.clientWidth; 491 if (maxScroll > 0 && viewport.scrollLeft >= maxScroll - 1) { 492 updateState(items.length - 1); 493 return; 494 } 495 var vLeft = viewport.getBoundingClientRect().left; 496 var closest = 0; 497 var minDist = Infinity; 498 items.forEach(function (item, i) { 499 var dist = Math.abs(item.getBoundingClientRect().left - vLeft); 500 if (dist < minDist) { minDist = dist; closest = i; } 501 }); 502 updateState(closest); 503 } 504 505 function reset() { 506 viewport.scrollLeft = 0; 507 updateState(0); 508 } 509 510 function wireDots() { 511 dots.forEach(function (dot, i) { 512 if (!dot || dot.getAttribute(DOT_INIT_ATTR) === 'true') return; 513 514 dot.setAttribute(DOT_INIT_ATTR, 'true'); 515 dot.addEventListener('click', function () { goTo(i * perPage); }); 516 dot.addEventListener('keydown', function (event) { 517 var visibleDots = getVisibleDots(); 518 var currentDotIndex = visibleDots.indexOf(dot); 519 var nextDotIndex = currentDotIndex; 520 521 if (!visibleDots.length || currentDotIndex === -1) return; 522 523 switch (event.key) { 524 case 'ArrowRight': 525 case 'ArrowDown': 526 nextDotIndex = (currentDotIndex + 1) % visibleDots.length; 527 break; 528 case 'ArrowLeft': 529 case 'ArrowUp': 530 nextDotIndex = (currentDotIndex - 1 + visibleDots.length) % visibleDots.length; 531 break; 532 case 'Home': 533 nextDotIndex = 0; 534 break; 535 case 'End': 536 nextDotIndex = visibleDots.length - 1; 537 break; 538 default: 539 return; 540 } 541 542 event.preventDefault(); 543 goTo(getDotPageIndex(dots.indexOf(visibleDots[nextDotIndex])) * perPage); 544 safeFocus(visibleDots[nextDotIndex]); 545 }); 546 }); 547 } 548 549 viewport.addEventListener('scroll', function () { 550 if (ticking) return; 551 ticking = true; 552 window.requestAnimationFrame(function () { syncFromScroll(); ticking = false; }); 553 }, { passive: true }); 554 555 if (prevBtn) prevBtn.addEventListener('click', function () { 556 goTo(Math.max(0, current - perPage)); 557 }); 558 if (nextBtn) nextBtn.addEventListener('click', function () { 559 goTo(Math.min(items.length - 1, current + perPage)); 560 }); 561 wireDots(); 562 563 function setDots(newDots, newClass) { 564 dots = newDots || []; 565 if (newClass) dotClass = newClass; 566 dotWindowStart = 0; 567 wireDots(); 568 updateState(current); 569 } 570 571 function setPerPage(n) { 572 perPage = Math.max(1, n || 1); 573 updateState(current); 574 } 575 576 function sync() { syncFromScroll(); } 577 578 updateState(0); 579 return { goTo: goTo, reset: reset, updateState: updateState, sync: sync, setDots: setDots, setPerPage: setPerPage }; 580 } 581 582 function clearButtonProgressTimer(button) { 583 if (!button || !button.__enisaProgressTimer) return; 584 window.clearInterval(button.__enisaProgressTimer); 585 button.__enisaProgressTimer = null; 586 } 587 588 function resetButtonProgress(button) { 589 if (!button) return; 590 clearButtonProgressTimer(button); 591 button.style.removeProperty("--enisa-btn-loader-overlay-w"); 592 } 593 594 function startButtonProgress(button) { 595 var current = 8; 596 var target = 86; 597 598 if (!button) return; 599 600 resetButtonProgress(button); 601 button.style.setProperty("--enisa-btn-loader-overlay-w", current + "%"); 602 603 button.__enisaProgressTimer = window.setInterval(function () { 604 current = Math.min(target, current + Math.max((target - current) * 0.18, 1.4)); 605 button.style.setProperty("--enisa-btn-loader-overlay-w", current.toFixed(2) + "%"); 606 607 if (current >= target) { 608 clearButtonProgressTimer(button); 609 } 610 }, 160); 611 } 612 613 function completeButtonProgress(button) { 614 if (!button) return; 615 clearButtonProgressTimer(button); 616 button.style.setProperty("--enisa-btn-loader-overlay-w", "100%"); 617 } 618 619 window.ENISA = window.ENISA || {}; 620 window.ENISA.utils = { 621 BREAKPOINTS: BREAKPOINTS, 622 prefersReducedMotion: prefersReducedMotion, 623 isMobileViewport: isMobileViewport, 624 debounce: debounce, 625 getFocusableElements: getFocusableElements, 626 isElementVisible: isElementVisible, 627 safeFocus: safeFocus, 628 focusWithoutScroll: focusWithoutScroll,
629 focusIfActiveInside: focusIfActiveInside, 630 restoreScrollPosition: restoreScrollPosition, 631 scrollToElement: scrollToElement, 632 setSubtreeTabbable: setSubtreeTabbable, 633 setInert: setInert, 634 createScrollCarousel: createScrollCarousel, 635 startButtonProgress: startButtonProgress, 636 completeButtonProgress: completeButtonProgress, 637 resetButtonProgress: resetButtonProgress 638 }; 639})(); 640 641(function () { 642 "use strict"; 643 644 var MOBILE_CARDS_BP = 768; 645 646 function isMobileCardsViewport() { 647 if (window.matchMedia) return window.matchMedia("(max-width: 768px)").matches; 648 return window.innerWidth <= MOBILE_CARDS_BP; 649 } 650 651 window.ENISA = window.ENISA || {}; 652 window.ENISA.cardsCarousel = window.ENISA.cardsCarousel || {}; 653 window.ENISA.cardsCarousel.MOBILE_BP = MOBILE_CARDS_BP; 654 window.ENISA.cardsCarousel.isMobileViewport = isMobileCardsViewport; 655 window.ENISA.cardsCarousel.getCardOffset = function (viewport, item) { 656 if (!viewport || !item) return 0; 657 var vRect = viewport.getBoundingClientRect(); 658 var iRect = item.getBoundingClientRect(); 659 var styles = window.getComputedStyle ? window.getComputedStyle(viewport) : null; 660 var paddingLeft = styles ? parseFloat(styles.paddingLeft || "0") : 0; 661 var scrollPaddingLeft = styles 662 ? parseFloat(styles.scrollPaddingInlineStart || styles.scrollPaddingLeft) 663 : NaN; 664 var leadingGutter = isNaN(scrollPaddingLeft) ? (paddingLeft || 0) : scrollPaddingLeft; 665 666 return Math.max(0, iRect.left - vRect.left + viewport.scrollLeft - leadingGutter); 667 }; 668 window.ENISA.cardsCarousel.syncEqualHeights = function (items) { 669 if (!items || !items.length) return; 670 671 items.forEach(function (item) { 672 if (!item || !item.style) return; 673 item.style.minHeight = ""; 674 item.style.height = ""; 675 }); 676 677 var tallest = 0; 678 items.forEach(function (item) { 679 if (!item) return; 680 var nextHeight = Math.ceil(item.getBoundingClientRect().height); 681 if (nextHeight > tallest) tallest = nextHeight; 682 }); 683 684 if (!tallest) return; 685 686 items.forEach(function (item) { 687 if (!item || !item.style) return; 688 item.style.minHeight = tallest + "px"; 689 }); 690 }; 691})(); 692 693/* 694 ENISA | JS Module 695 Modulo: 02-skip-link 696 Descripcion: 697 Gestion robusta del skip link en Power Pages. 698 - El skip link apunta siempre a #main-content. 699 - No depende de que main sea el siguiente nodo tras header. 700 - Enfoca el primer contenido util dentro del main. 701 - Respeta modales de cookies abiertos. 702*/ 703(function () { 704 "use strict"; 705 706 var SKIP_SELECTOR = ".enisa-skip-link"; 707 var SKIP_TARGET_ID = "main-content"; 708 var SKIP_TARGET_HASH = "#" + SKIP_TARGET_ID; 709 710 var HEADER_SELECTOR = "[data-header-root], .enisa-header, header[role='banner'], body > header"; 711 var FOOTER_SELECTOR = ".enisa-footer, footer"; 712 var GLOBAL_NAV_SELECTOR = ".enisa-header, .enisa-nav, .navbar, .enisa-header__mobile-nav, [data-header-root]"; 713 var COOKIE_SELECTOR = "[data-cookie-consent], .cmp-cookie-consent"; 714 715 var INIT_ATTR = "data-skip-init"; 716 var FOCUS_ADDED_ATTR = "data-enisa-skip-focus-added"; 717 718 var LANDING_TARGET_SELECTOR = [ 719 "[data-enisa-skip-target]", 720 "[data-skip-target]", 721 "h1", 722 "h2", 723 "h3" 724 ].join(","); 725 726 var FOCUSABLE_SELECTOR = [ 727 "a[href]", 728 "button:not([disabled])", 729 "input:not([disabled]):not([type='hidden'])", 730 "select:not([disabled])", 731 "textarea:not([disabled])", 732 "summary", 733 "[tabindex]:not([tabindex='-1'])" 734 ].join(","); 735 736 var NON_CONTENT_SELECTOR = [ 737 "script", 738 "style", 739 "template", 740 "[hidden]", 741 "[aria-hidden='true']", 742 "[inert]", 743 "header", 744 "footer", 745 "nav", 746 "[role='navigation']", 747 "[role='banner']", 748 "[role='contentinfo']", 749 "[data-header-root]", 750 ".enisa-header", 751 ".enisa-nav", 752 ".navbar", 753 ".enisa-header__mobile-nav", 754 ".enisa-overlay", 755 "#searchOverlay", 756 ".enisa-global-breadcrumb", 757 ".breadcrumb", 758 ".cmp-cookie-consent", 759 "[data-cookie-consent]", 760 ".enisa-skip-link", 761 ".sr-only", 762 ".enisa-sr-only", 763 ".u-visually-hidden", 764 ".xrm-attribute", 765 ".xrm-editable-html" 766 ].join(","); 767 768 function toArray(list) { 769 return Array.prototype.slice.call(list || []); 770 } 771 772 function normalizeSpaces(value) { 773 return String(value || "").replace(/\s+/g, " ").trim(); 774 } 775 776 function isElementVisible(element) { 777 var style; 778 779 if (!element || element.nodeType !== 1) return false;
780 if (element.hidden || element.getAttribute("aria-hidden") === "true") return false; 781 if (element.closest("[hidden], [aria-hidden='true'], [inert]")) return false; 782 783 if (window.getComputedStyle) { 784 style = window.getComputedStyle(element); 785 if (style.display === "none" || style.visibility === "hidden") return false; 786 } 787 788 if (element.getClientRects && element.getClientRects().length === 0) return false; 789 790 return true; 791 } 792 793 function isNonContentElement(element) { 794 return !!( 795 element && 796 element.nodeType === 1 && 797 element.matches && 798 element.matches(NON_CONTENT_SELECTOR) 799 ); 800 } 801 802 function isInsideNonContent(element, root) { 803 var current = element; 804 805 while (current && current !== root && current.nodeType === 1) { 806 if (isNonContentElement(current)) return true; 807 current = current.parentElement; 808 } 809 810 return false; 811 } 812 813 function getUsefulText(element) { 814 var walker; 815 var parts = []; 816 817 if (!element) return ""; 818 819 if (!document.createTreeWalker) { 820 return normalizeSpaces(element.textContent); 821 } 822 823 walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, { 824 acceptNode: function (node) { 825 var parent = node.parentElement; 826 827 if (!parent || isInsideNonContent(parent, element)) { 828 return NodeFilter.FILTER_REJECT; 829 } 830 831 return normalizeSpaces(node.nodeValue).length > 0 832 ? NodeFilter.FILTER_ACCEPT 833 : NodeFilter.FILTER_REJECT; 834 } 835 }); 836 837 while (walker.nextNode()) { 838 parts.push(walker.currentNode.nodeValue); 839 } 840 841 return normalizeSpaces(parts.join(" ")); 842 } 843 844 function hasUsefulContent(element) { 845 if (!isElementVisible(element)) return false; 846 if (isNonContentElement(element)) return false; 847 848 return getUsefulText(element).length > 0; 849 } 850 851 function canUseAsLandingTarget(element, main) { 852 if (!element || !main) return false; 853 if (!hasUsefulContent(element)) return false; 854 if (isInsideNonContent(element, main)) return false; 855 856 return true; 857 } 858 859 function getHeader() { 860 return document.querySelector(HEADER_SELECTOR); 861 } 862 863 function getFooter() { 864 return document.querySelector(FOOTER_SELECTOR); 865 } 866 867 function getMain() { 868 return document.getElementById(SKIP_TARGET_ID); 869 } 870 871 function getHeaderOffset() { 872 var header = getHeader(); 873 var style; 874 875 if (!header || !header.getBoundingClientRect) return 0; 876 877 style = window.getComputedStyle ? window.getComputedStyle(header) : null; 878 879 if (!style || (style.position !== "fixed" && style.position !== "sticky")) { 880 return 0; 881 } 882 883 return Math.ceil(header.getBoundingClientRect().height); 884 } 885 886 function prefersReducedMotion() { 887 return !!( 888 window.matchMedia && 889 window.matchMedia("(prefers-reduced-motion: reduce)").matches 890 ); 891 } 892 893 function getOpenCookieDialog() { 894 var dialogs = toArray(document.querySelectorAll(COOKIE_SELECTOR)); 895 896 for (var i = 0; i < dialogs.length; i += 1) { 897 if (isElementVisible(dialogs[i])) return dialogs[i]; 898 } 899 900 return null; 901 } 902 903 function ensureProgrammaticFocus(element) { 904 if (!element || typeof element.focus !== "function") return false; 905 906 if (!element.hasAttribute("tabindex")) { 907 element.setAttribute("tabindex", "-1"); 908 element.setAttribute(FOCUS_ADDED_ATTR, "true"); 909 } 910 911 return true; 912 } 913 914 function focusElement(element) { 915 if (!ensureProgrammaticFocus(element)) return false; 916 917 if ( 918 window.ENISA && 919 window.ENISA.utils && 920 typeof window.ENISA.utils.safeFocus === "function" 921 ) { 922 return window.ENISA.utils.safeFocus(element); 923 } 924 925 try { 926 element.focus({ preventScroll: true }); 927 } catch (error) { 928 element.focus(); 929 } 930 931 return document.activeElement === element; 932 } 933 934 function scrollToLandingTarget(target) { 935 var headerOffset; 936 var top; 937 var behavior; 938 939 if (!target || !target.getBoundingClientRect) return false; 940 941 if ( 942 window.ENISA && 943 window.ENISA.utils && 944 typeof window.ENISA.utils.scrollToElement === "function" 945 ) { 946 return window.ENISA.utils.scrollToElement(target, { block: "start" }); 947 } 948 949 headerOffset = getHeaderOffset(); 950 top = Math.max( 951 0, 952 target.getBoundingClientRect().top + (window.pageYOffset || 0) - headerOffset - 16 953 ); 954 behavior = prefersReducedMotion() ? "auto" : "smooth"; 955 956 try { 957 window.scrollTo({ 958 top: top, 959 left: window.pageXOffset || 0, 960 behavior: behavior 961 }); 962 } catch (error) { 963 window.scrollTo(window.pageXOffset || 0, top); 964 } 965 966 return true; 967 } 968 969 function focusCookieDialog(dialog) { 970 var target = null; 971 972 if (!dialog) return false;
973 974 if ( 975 window.ENISA && 976 window.ENISA.utils && 977 typeof window.ENISA.utils.getFocusableElements === "function" 978 ) { 979 target = window.ENISA.utils.getFocusableElements(dialog)[0] || null; 980 } 981 982 if (!target && dialog.querySelectorAll) { 983 target = toArray(dialog.querySelectorAll(FOCUSABLE_SELECTOR)).filter(isElementVisible)[0] || null; 984 } 985 986 target = target || dialog; 987 988 if (target === dialog && !dialog.hasAttribute("tabindex")) { 989 dialog.setAttribute("tabindex", "-1"); 990 } 991 992 return focusElement(target); 993 } 994 995 function findFirstUsefulHeading(scope, main) { 996 var headings; 997 998 if (!scope || !scope.querySelectorAll) return null; 999 1000 headings = toArray(scope.querySelectorAll(LANDING_TARGET_SELECTOR)); 1001 1002 for (var i = 0; i < headings.length; i += 1) { 1003 if (canUseAsLandingTarget(headings[i], main)) { 1004 return headings[i]; 1005 } 1006 } 1007 1008 return null; 1009 } 1010 1011 function findFirstUsefulDescendant(root, main) { 1012 var children; 1013 var child; 1014 var heading; 1015 var nested; 1016 1017 if (!root || !root.children) return null; 1018 1019 children = toArray(root.children); 1020 1021 for (var i = 0; i < children.length; i += 1) { 1022 child = children[i]; 1023 1024 if (!isElementVisible(child) || isNonContentElement(child)) { 1025 continue; 1026 } 1027 1028 if (canUseAsLandingTarget(child, main)) { 1029 if (child.matches && child.matches(LANDING_TARGET_SELECTOR)) { 1030 return child; 1031 } 1032 1033 heading = findFirstUsefulHeading(child, main); 1034 return heading || child; 1035 } 1036 1037 nested = findFirstUsefulDescendant(child, main); 1038 if (nested) return nested; 1039 } 1040 1041 return null; 1042 } 1043 1044 function getSkipLandingTarget(main) { 1045 var explicitTarget; 1046 var usefulDescendant; 1047 var heading; 1048 1049 if (!main) return null; 1050 1051 explicitTarget = main.querySelector("[data-enisa-skip-target], [data-skip-target]"); 1052 if (explicitTarget && canUseAsLandingTarget(explicitTarget, main)) { 1053 return explicitTarget; 1054 } 1055 1056 heading = findFirstUsefulHeading(main, main); 1057 if (heading) return heading; 1058 1059 usefulDescendant = findFirstUsefulDescendant(main, main); 1060 if (usefulDescendant) return usefulDescendant; 1061 1062 return main; 1063 } 1064 1065 function ensureSkipLinkHref(link) { 1066 if (!link) return; 1067 1068 if (link.getAttribute("href") !== SKIP_TARGET_HASH) { 1069 link.setAttribute("href", SKIP_TARGET_HASH); 1070 } 1071 } 1072 1073 function ensureSkipLinkBeforeHeader(link) { 1074 var header = getHeader(); 1075 1076 if (!link || !header || !document.body) return; 1077 1078 if (link.compareDocumentPosition(header) & Node.DOCUMENT_POSITION_FOLLOWING) { 1079 return; 1080 } 1081 1082 document.body.insertBefore(link, header); 1083 } 1084 1085 function activateSkipLink(event) { 1086 var cookieDialog; 1087 var main; 1088 var landingTarget; 1089 1090 if (event) { 1091 event.preventDefault(); 1092 } 1093 1094 cookieDialog = getOpenCookieDialog(); 1095 1096 if (cookieDialog) { 1097 focusCookieDialog(cookieDialog); 1098 return; 1099 } 1100 1101 main = getMain(); 1102 1103 if (!main) { 1104 return; 1105 } 1106 1107 landingTarget = getSkipLandingTarget(main) || main; 1108 1109 if (!focusElement(landingTarget)) { 1110 focusElement(main); 1111 landingTarget = main; 1112 } 1113 1114 scrollToLandingTarget(landingTarget); 1115 } 1116 1117 function getHeadLeaksBeforeMain() { 1118 var main = getMain(); 1119 var leaks; 1120 1121 if (!main || !document.body) return []; 1122 1123 leaks = toArray( 1124 document.body.querySelectorAll( 1125 ":scope > meta, :scope > title, :scope > head, :scope > body, :scope > script[src*='googletagmanager']" 1126 ) 1127 ); 1128 1129 return leaks 1130 .filter(function (element) { 1131 return !!(element.compareDocumentPosition(main) & Node.DOCUMENT_POSITION_FOLLOWING); 1132 }) 1133 .map(function (element) { 1134 return { 1135 tagName: element.tagName.toLowerCase(), 1136 id: element.id || "", 1137 src: element.getAttribute("src") || "", 1138 text: normalizeSpaces(element.textContent).slice(0, 120) 1139 }; 1140 }); 1141 } 1142 1143 function getVisibleMainLikeElements() { 1144 return toArray(document.querySelectorAll("main, [role='main']")).filter(isElementVisible); 1145 } 1146 1147 function audit() { 1148 var links = toArray(document.querySelectorAll(SKIP_SELECTOR)); 1149 var link = links[0] || null; 1150 var main = getMain(); 1151 var header = getHeader(); 1152 var footer = getFooter(); 1153 var cookieDialog = getOpenCookieDialog(); 1154 var landingTarget = main ? getSkipLandingTarget(main) : null; 1155 var headLeaksBeforeMain = getHeadLeaksBeforeMain(); 1156 var visibleMainLikeElements = getVisibleMainLikeElements(); 1157 1158 return { 1159 skipLinks: links.length, 1160 skipHref: link ? link.getAttribute("href") : null, 1161 skipTargetsMain: !!(link && main && document.querySelector(link.getAttribute("href")) === main), 1162 skipLinkBeforeHeader: !!( 1163 link && 1164 header && 1165 (link.compareDocumentPosition(header) & Node.DOCUMENT_POSITION_FOLLOWING) 1166 ), 1167 mainExists: !!main, 1168 mainCountById: document.querySelectorAll("#" + SKIP_TARGET_ID).length, 1169 visibleMainLikeCount: visibleMainLikeElements.length, 1170 mainHasTabindexMinusOne: !!(main && main.getAttribute("tabindex") === "-1"), 1171 headerInsideMain: !!(main && header && main.contains(header)), 1172 globalNavigationInsideMain: !!(main && main.querySelector(GLOBAL_NAV_SELECTOR)), 1173 footerInsideMain: !!(main && footer && main.contains(footer)), 1174 cookieDialogOpen: !!cookieDialog, 1175 bodyHasHeadLeak: headLeaksBeforeMain.length > 0, 1176 headLeaksBeforeMain: headLeaksBeforeMain, 1177 landingTarget: landingTarget 1178 ? { 1179 tagName: landingTarget.tagName.toLowerCase(), 1180 id: landingTarget.id || "", 1181 className: String(landingTarget.className || ""), 1182 text: normalizeSpaces(landingTarget.textContent).slice(0, 120) 1183 } 1184 : null 1185 }; 1186 } 1187 1188 function initLink(link) { 1189 if (!link) return; 1190 1191 ensureSkipLinkHref(link); 1192 ensureSkipLinkBeforeHeader(link); 1193 1194 if (link.getAttribute(INIT_ATTR) === "true") return; 1195 1196 link.setAttribute(INIT_ATTR, "true"); 1197 link.addEventListener("click", activateSkipLink); 1198 } 1199
1200 function init() { 1201 var links = document.querySelectorAll(SKIP_SELECTOR); 1202 1203 toArray(links).forEach(initLink); 1204 1205 window.ENISA = window.ENISA || {}; 1206 window.ENISA.skipLink = { 1207 activate: activateSkipLink, 1208 audit: audit 1209 }; 1210 } 1211 1212 if (document.readyState === "loading") { 1213 document.addEventListener("DOMContentLoaded", init); 1214 } else { 1215 init(); 1216 } 1217})(); 1218 1219/* 1220 ENISA | JS Module 1221 Modulo: 03-header 1222 Descripcion: Logica del modulo. 1223*/ 1224(function () { 1225 "use strict"; 1226 var DESKTOP_NAV_BREAKPOINT = 1023.98; 1227 var PRESEARCH_MIN_CHARS = 4; 1228 var SEARCH_FOCUS_INTENT_KEY = "enisa:global-search:focus-results"; 1229 1230 function initHeader() { 1231 var header = document.querySelector("[data-header-root]") || document.querySelector(".enisa-header"); 1232 if (!header) return; 1233 1234 var root = document.documentElement; 1235 var headerBar = header.querySelector(".enisa-header__bar"); 1236 var headerContainer = header.querySelector(".enisa-header__container"); 1237 var overlay = 1238 document.getElementById("searchOverlay") || 1239 header.querySelector("#searchOverlay") || 1240 header.querySelector(".enisa-overlay"); 1241 1242 function ensureOverlayMarkup() { 1243 if (!overlay) return; 1244 1245 if (!overlay.querySelector(".enisa-overlay__panel")) { 1246 overlay.innerHTML = [ 1247 '<div class="enisa-overlay__panel">', 1248 ' <div class="enisa-overlay__body">', 1249 ' <button class="enisa-overlay__close js-close-search" type="button" aria-label="Cerrar búsqueda">', 1250 ' <img class="enisa-overlay__close-icon" src="/assets/icons/close.svg" alt="" width="20" height="20" aria-hidden="true">', 1251 ' </button>', 1252 ' <h2 id="enisa-search-title" class="enisa-overlay__title">¿En qué podemos <span class="enisa-overlay__title-mark">ayudarte</span><span class="enisa-overlay__title-punctuation">?</span></h2>', 1253 ' <form class="enisa-overlay__form" action="/buscar" autocomplete="off" method="get" role="search" novalidate>', 1254 ' <div class="enisa-overlay__field">', 1255 ' <label class="sr-only" for="SearchTerm">Buscar</label>', 1256 ' <input class="enisa-overlay__input" id="SearchTerm" name="q" type="search" value="" placeholder="Buscar" autocomplete="off" maxlength="200">', 1257 ' <input type="hidden" name="SearchTerm" value="">', 1258 ' <div class="enisa-overlay__actions">', 1259 ' <button class="enisa-overlay__action enisa-overlay__action--clear js-search-clear" type="button" aria-label="Borrar búsqueda" hidden>', 1260 ' <img class="enisa-overlay__action-icon" src="/assets/icons/close.svg" alt="" width="20" height="20" aria-hidden="true">', 1261 ' </button>', 1262 ' <button class="enisa-overlay__action enisa-overlay__action--submit" type="submit" aria-label="Buscar">', 1263 ' <img class="enisa-overlay__action-icon" src="/assets/icons/search.svg" alt="" width="20" height="20" aria-hidden="true">', 1264 ' </button>', 1265 ' </div>', 1266 " </div>", 1267 " </form>", 1268 ' <p id="presearch-status" class="sr-only" aria-live="polite" aria-atomic="true"></p>', 1269 ' <div id="presearch-result" class="enisa-overlay__results" role="list"></div>', 1270 " </div>", 1271 "</div>", 1272 ].join(""); 1273 } 1274 1275 var body = overlay.querySelector(".enisa-overlay__body") || overlay; 1276 var title = overlay.querySelector(".enisa-overlay__title"); 1277 var result = overlay.querySelector("#presearch-result, .enisa-overlay__results"); 1278 1279 if (!overlay.querySelector(".js-close-search")) { 1280 var close = document.createElement("button"); 1281 close.className = "enisa-overlay__close js-close-search"; 1282 close.type = "button"; 1283 close.setAttribute("aria-label", "Cerrar búsqueda"); 1284 close.innerHTML = '<img class="enisa-overlay__close-icon" src="/assets/icons/close.svg" alt="" width="20" height="20" aria-hidden="true">'; 1285 body.insertBefore(close, title || body.firstChild); 1286 } 1287 1288 if (!overlay.querySelector("#presearch-status")) { 1289 var status = document.createElement("p"); 1290 status.id = "presearch-status"; 1291 status.className = "sr-only"; 1292 status.setAttribute("role", "status"); 1293 status.setAttribute("aria-live", "polite"); 1294 status.setAttribute("aria-atomic", "true"); 1295 body.insertBefore(status, result || null); 1296 } 1297 } 1298 1299 ensureOverlayMarkup(); 1300 1301 var openBtns = [].slice.call( 1302 header.querySelectorAll(".js-toggle-search, [aria-controls='searchOverlay']") 1303 ); 1304 var closeBtn = header.querySelector(".js-close-search"); 1305 var searchInput = document.getElementById("SearchTerm"); 1306 var searchClearBtn = 1307 header.querySelector(".js-search-clear") || 1308 (overlay ? overlay.querySelector(".js-search-clear") : null); 1309 var searchSubmitBtn = overlay 1310 ? overlay.querySelector(".enisa-overlay__action--submit") 1311 : null; 1312 var presearchResult = document.getElementById("presearch-result"); 1313 var presearchStatus = document.getElementById("presearch-status"); 1314 var overlayForm = overlay ? overlay.querySelector(".enisa-overlay__form") : null; 1315 var overlayCompatSearchInput = 1316 overlayForm && overlayForm.querySelector("input[name='SearchTerm']"); 1317 var mobileSearchForms = [].slice.call( 1318 header.querySelectorAll( 1319 ".enisa-header__mobile-search-box, .enisa-header__mobile-help form[action*='buscar']" 1320 ) 1321 ); 1322 var dropdownToggles = [].slice.call( 1323 header.querySelectorAll("[data-bs-toggle='dropdown']") 1324 ); 1325 var dropdownMenus = [].slice.call( 1326 header.querySelectorAll(".enisa-nav__dropdown.dropdown-menu") 1327 ); 1328 var mobileToggles = [].slice.call( 1329 header.querySelectorAll(".js-mobile-menu-toggle") 1330 ); 1331 var mobilePanel = document.getElementById("headerMobilePanel"); 1332 var mobileDetails = [].slice.call( 1333 header.querySelectorAll(".enisa-header__mobile-details") 1334 ); 1335 var mobileLockScrollY = 0; 1336 var mobileLockScrollX = 0; 1337 var lastScrollY = window.scrollY || window.pageYOffset || 0; 1338 var lastSearchTrigger = null; 1339 var presearchDebounce = null; 1340 var presearchReqSeq = 0; 1341 var presearchAnnounced = "";
1342 var searchBackgroundNodes = []; 1343 var mobileBackgroundNodes = []; 1344 1345 function getFocusableElements(scope) { 1346 if ( 1347 window.ENISA && 1348 window.ENISA.utils && 1349 typeof window.ENISA.utils.getFocusableElements === "function" 1350 ) { 1351 return window.ENISA.utils.getFocusableElements(scope); 1352 } 1353 1354 return [].slice.call( 1355 scope.querySelectorAll( 1356 "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])" 1357 ) 1358 ).filter(function (el) { 1359 return !el.hasAttribute("hidden") && el.offsetParent !== null; 1360 }); 1361 } 1362 1363 function safeFocus(element) { 1364 if ( 1365 window.ENISA && 1366 window.ENISA.utils && 1367 typeof window.ENISA.utils.safeFocus === "function" 1368 ) { 1369 return window.ENISA.utils.safeFocus(element); 1370 } 1371 1372 if (!element || typeof element.focus !== "function") return false; 1373 1374 try { 1375 element.focus({ preventScroll: true }); 1376 } catch (error) { 1377 element.focus(); 1378 } 1379 1380 return true; 1381 } 1382 1383 function setInert(element, isInert) { 1384 if ( 1385 window.ENISA && 1386 window.ENISA.utils && 1387 typeof window.ENISA.utils.setInert === "function" 1388 ) { 1389 window.ENISA.utils.setInert(element, isInert); 1390 } 1391 } 1392 1393 function setNodesInert(nodes, isInert) { 1394 nodes.forEach(function (node) { 1395 setInert(node, isInert); 1396 }); 1397 } 1398 1399 function collectBodySiblings(exceptElement) { 1400 return [].slice.call(document.body.children).filter(function (child) { 1401 return child !== exceptElement && !child.contains(exceptElement); 1402 }); 1403 } 1404 1405 function collectHeaderSiblings(exceptElement) { 1406 if (!exceptElement || !exceptElement.parentElement) return []; 1407 1408 return [].slice.call(exceptElement.parentElement.children).filter(function (child) { 1409 return child !== exceptElement && !child.contains(exceptElement); 1410 }); 1411 } 1412
1413 function setSearchBackgroundHidden(isHidden) { 1414 if (!overlay) return; 1415 1416 if (isHidden) { 1417 searchBackgroundNodes = collectBodySiblings(header).concat(collectHeaderSiblings(overlay)); 1418 setNodesInert(searchBackgroundNodes, true); 1419 return; 1420 } 1421 1422 setNodesInert(searchBackgroundNodes, false); 1423 searchBackgroundNodes = []; 1424 } 1425 1426 function setMobileBackgroundHidden(isHidden) { 1427 if (!mobilePanel) return; 1428 1429 if (isHidden) { 1430 mobileBackgroundNodes = collectBodySiblings(header).concat(collectHeaderSiblings(mobilePanel)); 1431 setNodesInert(mobileBackgroundNodes, true); 1432 return; 1433 } 1434 1435 setNodesInert(mobileBackgroundNodes, false); 1436 mobileBackgroundNodes = []; 1437 } 1438 1439 function ensurePresearchStatus() { 1440 if (presearchStatus || !overlay) return; 1441 1442 presearchStatus = document.createElement("p"); 1443 presearchStatus.id = "presearch-status"; 1444 presearchStatus.className = "sr-only"; 1445 presearchStatus.setAttribute("aria-live", "polite"); 1446 presearchStatus.setAttribute("aria-atomic", "true"); 1447 1448 if (presearchResult && presearchResult.parentNode) { 1449 presearchResult.parentNode.insertBefore(presearchStatus, presearchResult); 1450 } else { 1451 overlay.appendChild(presearchStatus); 1452 } 1453 } 1454 1455 function normalizeSearchValue(value) { 1456 return (value || "").replace(/\s+/g, " ").trim(); 1457 } 1458 1459 function isTrustedUserAction(event) { 1460 return !event || event.isTrusted !== false; 1461 } 1462 1463 function markSearchSubmitIntent() { 1464 try { 1465 window.sessionStorage.setItem(SEARCH_FOCUS_INTENT_KEY, "1"); 1466 } catch (error) {} 1467 } 1468 1469 function setMobileTogglesExpanded(isExpanded) { 1470 mobileToggles.forEach(function (toggleBtn) { 1471 if (toggleBtn.hasAttribute("aria-expanded")) { 1472 toggleBtn.setAttribute("aria-expanded", isExpanded ? "true" : "false"); 1473 } 1474 }); 1475 } 1476 1477 function syncSearchCompatInputs(value) { 1478 var normalized = normalizeSearchValue(value); 1479 if (overlayCompatSearchInput) { 1480 overlayCompatSearchInput.value = normalized; 1481 } 1482 1483 mobileSearchForms.forEach(function (form) { 1484 var compatInput = form.querySelector("input[name='SearchTerm']"); 1485 var visibleInput = form.querySelector("input[name='q']"); 1486 if (visibleInput && visibleInput.value !== normalized) { 1487 visibleInput.value = normalized; 1488 } 1489 if (compatInput) { 1490 compatInput.value = normalized; 1491 } 1492 }); 1493 } 1494 1495 function renderPresearch(items) { 1496 if (!presearchResult) return; 1497 presearchResult.innerHTML = ""; 1498 1499 if (!items.length) { 1500 presearchResult.classList.remove("is-visible"); 1501 if (presearchStatus) presearchStatus.textContent = ""; 1502 return; 1503 } 1504 1505 var frag = document.createDocumentFragment(); 1506 var addedCount = 0; 1507 1508 items.forEach(function (item) { 1509 var name = typeof item === "string" ? item : (item && item.name || ""); 1510 var url = (typeof item === "object" && item) ? (item.url || null) : null; 1511 var normalizedName = normalizeSearchValue(name); 1512 if (!normalizedName) return; 1513 1514 var row = document.createElement("div"); 1515 row.className = "enisa-overlay__result-item"; 1516 row.setAttribute("role", "listitem"); 1517 1518 if (url) { 1519 var link = document.createElement("a"); 1520 link.className = "enisa-overlay__result-button"; 1521 link.href = url; 1522 link.textContent = normalizedName; 1523 row.appendChild(link); 1524 } else { 1525 var form = document.createElement("form"); 1526 form.className = "enisa-overlay__result-form"; 1527 form.action = 1528 (overlayForm && overlayForm.getAttribute("action")) || "/buscar"; 1529 form.method = 1530 (overlayForm && overlayForm.getAttribute("method")) || "get"; 1531 form.setAttribute("autocomplete", "off"); 1532 1533 var qInput = document.createElement("input"); 1534 qInput.type = "hidden"; 1535 qInput.name = "q"; 1536 qInput.value = normalizedName; 1537 1538 var compatInput = document.createElement("input"); 1539 compatInput.type = "hidden"; 1540 compatInput.name = "SearchTerm"; 1541 compatInput.value = normalizedName; 1542 1543 var btn = document.createElement("button"); 1544 btn.className = "enisa-overlay__result-button"; 1545 btn.type = "submit"; 1546 btn.textContent = normalizedName; 1547 btn.addEventListener("click", markSearchSubmitIntent); 1548 1549 form.appendChild(qInput); 1550 form.appendChild(compatInput); 1551 form.appendChild(btn); 1552 row.appendChild(form); 1553 } 1554 1555 frag.appendChild(row); 1556 addedCount += 1; 1557 }); 1558 1559 presearchResult.appendChild(frag); 1560 var visible = presearchResult.children.length > 0; 1561 presearchResult.classList.toggle("is-visible", visible); 1562 if (presearchStatus) { 1563 var message = visible 1564 ? addedCount + (addedCount === 1 ? " sugerencia encontrada" : " sugerencias encontradas") 1565 : ""; 1566 if (message !== presearchAnnounced) { 1567 presearchStatus.textContent = message; 1568 presearchAnnounced = message; 1569 } 1570 } 1571 } 1572 1573 function clearPresearch() { 1574 if (!presearchResult) return; 1575 presearchResult.innerHTML = ""; 1576 presearchResult.classList.remove("is-visible"); 1577 if (presearchStatus) presearchStatus.textContent = ""; 1578 presearchAnnounced = ""; 1579 } 1580 1581 function getPresearchActions() { 1582 if (!presearchResult) return []; 1583 1584 return [].slice.call( 1585 presearchResult.querySelectorAll("a[href], button:not([disabled])") 1586 ).filter(function (element) { 1587 return element.offsetParent !== null; 1588 }); 1589 } 1590 1591 function updateOverlaySearchUi() { 1592 if (!searchInput) return; 1593 var hasText = normalizeSearchValue(searchInput.value).length > 0; 1594 if (searchClearBtn) { 1595 searchClearBtn.hidden = !hasText; 1596 } 1597 if (!hasText) { 1598 clearPresearch(); 1599 } 1600 } 1601 1602 function parsePresearchPayload(payload) { 1603 if (!payload) return []; 1604 if (typeof payload === "string") { 1605 var raw = payload.trim(); 1606 try { 1607 payload = JSON.parse(raw); 1608 } catch (e) { 1609 var tmp = document.createElement("div"); 1610 tmp.innerHTML = raw; 1611 var htmlTerms = []; 1612 var hiddenTerms = [].slice.call( 1613 tmp.querySelectorAll("input[name='SearchTerm']") 1614 ).map(function (input) { 1615 return (input.value || "").trim(); 1616 }); 1617 var buttonTerms = [].slice 1618 .call(tmp.querySelectorAll("button")) 1619 .map(function (btn) { 1620 return (btn.textContent || "").trim(); 1621 });
1622 htmlTerms = hiddenTerms.concat(buttonTerms).filter(Boolean); 1623 return htmlTerms 1624 .filter(function (term, index, arr) { 1625 return arr.indexOf(term) === index; 1626 }) 1627 .map(function (term) { 1628 return { name: term, url: null }; 1629 }) 1630 .slice(0, 18); 1631 } 1632 } 1633 1634 if (Array.isArray(payload)) { 1635 return payload 1636 .map(function (item) { 1637 if (typeof item === "string") return { name: item, url: null }; 1638 var name = item && (item.Name || item.name || item.term || item.Term) || ""; 1639 var url = item && (item.Url || item.url || item.URL) || null; 1640 return name ? { name: name, url: url || null } : null; 1641 }) 1642 .filter(Boolean) 1643 .slice(0, 18); 1644 } 1645 1646 if (payload.results && Array.isArray(payload.results)) { 1647 return parsePresearchPayload(payload.results); 1648 } 1649 1650 if (payload.value && Array.isArray(payload.value)) { 1651 return parsePresearchPayload(payload.value); 1652 } 1653 1654 if (payload.d) { 1655 return parsePresearchPayload(payload.d); 1656 } 1657 1658 return []; 1659 } 1660 1661 function fetchPresearch(term) { 1662 var currentReq = ++presearchReqSeq; 1663 var url = "/buscar/presearch?q=" + encodeURIComponent(term); 1664 1665 fetch(url, { 1666 method: "GET", 1667 credentials: "same-origin", 1668 }) 1669 .then(function (res) { 1670 if (!res.ok) throw new Error("presearch request failed"); 1671 return res.text(); 1672 }) 1673 .then(function (rawPayload) { 1674 if (currentReq !== presearchReqSeq) return; 1675 renderPresearch(parsePresearchPayload(rawPayload)); 1676 }) 1677 .catch(function () { 1678 if (currentReq !== presearchReqSeq) return; 1679 clearPresearch(); 1680 }); 1681 } 1682 1683 function schedulePresearch() { 1684 if (!searchInput) return; 1685 var normalized = normalizeSearchValue(searchInput.value); 1686 syncSearchCompatInputs(normalized); 1687 updateOverlaySearchUi(); 1688 1689 clearTimeout(presearchDebounce); 1690 1691 if (normalized.length < PRESEARCH_MIN_CHARS) { 1692 clearPresearch(); 1693 return; 1694 } 1695 1696 presearchDebounce = setTimeout(function () { 1697 fetchPresearch(normalized); 1698 }, 180); 1699 } 1700 1701 function parsePx(value) { 1702 var parsed = parseFloat(value || "0"); 1703 return Number.isFinite(parsed) ? parsed : 0; 1704 } 1705 1706 function measureActionLabelWidth(label) { 1707 if (!label || !label.textContent) return 0; 1708 1709 var probe = document.createElement("span"); 1710 probe.className = "enisa-header__action-label"; 1711 probe.textContent = normalizeSearchValue(label.textContent); 1712 probe.style.position = "absolute"; 1713 probe.style.left = "-9999px"; 1714 probe.style.top = "0"; 1715 probe.style.maxWidth = "none"; 1716 probe.style.opacity = "1"; 1717 probe.style.transform = "none"; 1718 probe.style.visibility = "hidden"; 1719 probe.style.pointerEvents = "none"; 1720 document.body.appendChild(probe); 1721 1722 var width = Math.ceil(probe.getBoundingClientRect().width); 1723 probe.remove(); 1724 return width; 1725 } 1726 1727 function syncHeaderActionWidths() { 1728 var revealActions = [].slice.call( 1729 header.querySelectorAll( 1730 ".enisa-header__actions--desktop .enisa-header__action--reveal:not(.enisa-header__action--help)" 1731 ) 1732 ); 1733 1734 if (!revealActions.length) return; 1735 1736 var rootStyles = window.getComputedStyle(root); 1737 var actionSize = parsePx( 1738 rootStyles.getPropertyValue("--enisa-header-action-size-desktop") 1739 ); 1740 var iconSize = parsePx( 1741 rootStyles.getPropertyValue("--enisa-header-action-icon-size") 1742 ); 1743 var rightPad = 14; 1744 var gap = 4; 1745 var leftPad = Math.max(0, (actionSize - iconSize) / 2); 1746 1747 revealActions.forEach(function (action) { 1748 var label = action.querySelector(".enisa-header__action-label"); 1749 if (!label) return; 1750 1751 var labelWidth = measureActionLabelWidth(label); 1752 var expandedWidth = Math.ceil( 1753 leftPad + iconSize + gap + labelWidth + rightPad + 4 1754 ); 1755 1756 action.style.setProperty( 1757 "--enisa-header-action-expand-width", 1758 expandedWidth + "px" 1759 ); 1760 action.style.setProperty( 1761 "--enisa-header-action-label-max", 1762 labelWidth + "px" 1763 ); 1764 }); 1765 } 1766 1767 function syncHeaderMetrics() { 1768 header.style.removeProperty("--enisa-header-open-offset"); 1769 root.style.removeProperty("--enisa-header-open-offset"); 1770 } 1771 1772 function toggleScrolledHeader() { 1773 var currentScrollY = window.scrollY || window.pageYOffset || 0; 1774 var isOverlayOpen = overlay && !overlay.hidden; 1775 var isMobilePanelOpen = mobilePanel && !mobilePanel.hidden; 1776 var isDropdownOpen = header.classList.contains("has-open-dropdown"); 1777 1778 header.classList.toggle("is-scrolled", currentScrollY > 8); 1779 1780 if ( 1781 currentScrollY <= 8 || 1782 isOverlayOpen || 1783 isMobilePanelOpen || 1784 isDropdownOpen 1785 ) { 1786 header.classList.remove("is-scroll-down"); 1787 header.classList.add("is-scroll-up"); 1788 lastScrollY = currentScrollY; 1789 return; 1790 } 1791 1792 if (currentScrollY > lastScrollY + 2) { 1793 header.classList.add("is-scroll-down"); 1794 header.classList.remove("is-scroll-up"); 1795 } else if (currentScrollY < lastScrollY - 2) { 1796 header.classList.remove("is-scroll-down"); 1797 header.classList.add("is-scroll-up"); 1798 } 1799 1800 lastScrollY = currentScrollY; 1801 } 1802 1803 var navVeil = null; 1804 1805 function syncDropdownState() { 1806 var openItem = header.querySelector(".enisa-nav__item.dropdown.show"); 1807 header.classList.toggle("has-open-dropdown", !!openItem); 1808 document.body.classList.toggle("header-dropdown-is-open", !!openItem); 1809 1810 if (!navVeil) { 1811 navVeil = document.createElement("div"); 1812 navVeil.className = "enisa-nav-veil"; 1813 document.body.appendChild(navVeil); 1814 } 1815 1816 navVeil.classList.toggle("is-active", !!openItem); 1817 1818 if (openItem) { 1819 header.classList.remove("is-scroll-down"); 1820 header.classList.add("is-scroll-up"); 1821 } 1822 } 1823 1824 function openMobileMenu() { 1825 if (!mobilePanel || !mobileToggles.length) return; 1826 1827 mobileLockScrollY = window.scrollY || window.pageYOffset || 0; 1828 mobileLockScrollX = window.scrollX || window.pageXOffset || 0; 1829 mobilePanel.hidden = false;
1830 mobilePanel.setAttribute("aria-hidden", "false"); 1831 setMobileTogglesExpanded(true); 1832 header.classList.add("has-open-mobile-panel"); 1833 header.classList.remove("is-scroll-down"); 1834 header.classList.add("is-scroll-up"); 1835 document.body.classList.add("mobile-menu-is-open"); 1836 document.body.style.position = "fixed"; 1837 document.body.style.top = "-" + mobileLockScrollY + "px"; 1838 document.body.style.left = "0"; 1839 document.body.style.right = "0"; 1840 document.body.style.width = "100%"; 1841 1842 var firstFocusable = getFocusableElements(mobilePanel)[0]; 1843 1844 if (firstFocusable) { 1845 requestAnimationFrame(function () { 1846 safeFocus(firstFocusable); 1847 setMobileBackgroundHidden(true); 1848 }); 1849 } else { 1850 setMobileBackgroundHidden(true); 1851 } 1852 } 1853 1854 function closeMobileMenu(options) { 1855 var shouldRestoreFocus; 1856 var focusTarget; 1857 1858 if (!mobilePanel || !mobileToggles.length) return; 1859 1860 shouldRestoreFocus = !!(options && options.restoreFocus); 1861 focusTarget = (options && options.focusTarget) || mobileToggles[0]; 1862 1863 setMobileBackgroundHidden(false); 1864 1865 if (shouldRestoreFocus && focusTarget && typeof focusTarget.focus === "function") { 1866 safeFocus(focusTarget); 1867 } 1868 1869 mobilePanel.hidden = true; 1870 mobilePanel.setAttribute("aria-hidden", "true"); 1871 setMobileTogglesExpanded(false); 1872 header.classList.remove("has-open-mobile-panel"); 1873 document.body.classList.remove("mobile-menu-is-open"); 1874 document.body.style.position = ""; 1875 document.body.style.top = ""; 1876 document.body.style.left = ""; 1877 document.body.style.right = ""; 1878 document.body.style.width = ""; 1879 if ( 1880 window.ENISA && 1881 window.ENISA.utils && 1882 typeof window.ENISA.utils.restoreScrollPosition === "function" 1883 ) { 1884 window.ENISA.utils.restoreScrollPosition(mobileLockScrollY, mobileLockScrollX); 1885 } else { 1886 window.scrollTo(mobileLockScrollX, mobileLockScrollY); 1887 } 1888 } 1889 1890 function toggleMobileMenu() { 1891 if (!mobilePanel || !mobileToggles.length) return; 1892 1893 if (mobilePanel.hidden) { 1894 openMobileMenu(); 1895 } else { 1896 var activeIsInsidePanel = mobilePanel.contains(document.activeElement); 1897 closeMobileMenu({ restoreFocus: activeIsInsidePanel }); 1898 } 1899 } 1900 1901 function openSearch() { 1902 if (!overlay) return; 1903 1904 closeMobileMenu(); 1905 closeOpenDropdowns(false); 1906 1907 overlay.hidden = false; 1908 overlay.removeAttribute("hidden"); 1909 overlay.style.display = "block"; 1910 overlay.setAttribute("aria-hidden", "false"); 1911 syncSearchCompatInputs(searchInput ? searchInput.value : ""); 1912 updateOverlaySearchUi(); 1913 1914 openBtns.forEach(function (btn) { 1915 btn.setAttribute("aria-expanded", "true"); 1916 }); 1917 1918 document.body.classList.add("search-overlay-is-open"); 1919 1920 if (searchInput) { 1921 safeFocus(searchInput); 1922 } 1923 1924 setSearchBackgroundHidden(true); 1925 } 1926 1927 function closeSearch(restoreFocus) { 1928 if (!overlay) return; 1929 if (typeof restoreFocus === "undefined") restoreFocus = true; 1930 1931 clearTimeout(presearchDebounce); 1932 presearchReqSeq += 1; 1933 clearPresearch(); 1934 1935 openBtns.forEach(function (btn) { 1936 btn.setAttribute("aria-expanded", "false"); 1937 }); 1938 1939 document.body.classList.remove("search-overlay-is-open"); 1940 setSearchBackgroundHidden(false); 1941 1942 if (restoreFocus && lastSearchTrigger) { 1943 safeFocus(lastSearchTrigger); 1944 } 1945 1946 overlay.hidden = true; 1947 overlay.setAttribute("hidden", ""); 1948 overlay.style.display = ""; 1949 overlay.setAttribute("aria-hidden", "true"); 1950 } 1951 1952 function closeOpenDropdowns(restoreFocus) { 1953 if (typeof restoreFocus === "undefined") restoreFocus = true; 1954 1955 dropdownToggles.forEach(function (toggle) { 1956 var isExpanded = toggle.getAttribute("aria-expanded") === "true"; 1957 if (!isExpanded) return; 1958 1959 if (window.bootstrap && window.bootstrap.Dropdown) { 1960 var instance = window.bootstrap.Dropdown.getOrCreateInstance(toggle); 1961 instance.hide(); 1962 } else { 1963 var parentItem = toggle.closest("[data-nav-item]"); 1964 var menu = parentItem && parentItem.querySelector(".dropdown-menu"); 1965 if (parentItem) parentItem.classList.remove("show"); 1966 toggle.setAttribute("aria-expanded", "false"); 1967 if (menu) menu.classList.remove("show"); 1968 } 1969 1970 if (restoreFocus) { 1971 safeFocus(toggle); 1972 } 1973 }); 1974 } 1975 1976 syncHeaderMetrics(); 1977 syncHeaderActionWidths(); 1978 ensurePresearchStatus(); 1979 1980 if (document.fonts && document.fonts.ready) { 1981 document.fonts.ready.then(syncHeaderActionWidths); 1982 } 1983 1984 toggleScrolledHeader(); 1985 syncDropdownState(); 1986 syncSearchCompatInputs(searchInput ? searchInput.value : ""); 1987 updateOverlaySearchUi(); 1988 1989 window.addEventListener( 1990 "scroll", 1991 function () { 1992 toggleScrolledHeader(); 1993 syncHeaderMetrics(); 1994 }, 1995 { passive: true } 1996 ); 1997 1998 window.addEventListener("resize", window.ENISA.utils.debounce(function () { 1999 syncHeaderMetrics(); 2000 syncHeaderActionWidths(); 2001 2002 if (window.innerWidth > DESKTOP_NAV_BREAKPOINT) { 2003 closeMobileMenu(); 2004 } else { 2005 closeOpenDropdowns(false); 2006 } 2007 2008 syncDropdownState(); 2009 }, 150)); 2010 2011 openBtns.forEach(function (btn) { 2012 btn.addEventListener("click", function (e) { 2013 e.preventDefault(); 2014 lastSearchTrigger = btn; 2015 var isOpen = overlay && !overlay.hidden; 2016 2017 if (isOpen) { 2018 closeSearch(); 2019 } else { 2020 openSearch(); 2021 } 2022 }); 2023 }); 2024 2025 if (searchInput) { 2026 searchInput.addEventListener("input", schedulePresearch); 2027 searchInput.addEventListener("change", schedulePresearch); 2028 searchInput.addEventListener("keydown", function (e) { 2029 if (e.key === "Escape") { 2030 if (presearchResult && presearchResult.classList.contains("is-visible")) { 2031 e.stopPropagation(); 2032 } 2033 2034 clearPresearch(); 2035 return; 2036 } 2037 2038 if (e.key === "ArrowDown") { 2039 var actions = getPresearchActions(); 2040 if (!actions.length) return; 2041 2042 e.preventDefault(); 2043 safeFocus(actions[0]); 2044 } 2045 }); 2046 } 2047 2048 if (searchClearBtn && searchInput) { 2049 searchClearBtn.addEventListener("click", function (e) { 2050 e.preventDefault(); 2051 searchInput.value = ""; 2052 syncSearchCompatInputs(""); 2053 updateOverlaySearchUi(); 2054 clearPresearch(); 2055 safeFocus(searchInput); 2056 }); 2057 } 2058 2059 if (overlayForm && searchInput) { 2060 overlayForm.addEventListener("submit", function (event) { 2061 syncSearchCompatInputs(searchInput.value); 2062 2063 if (isTrustedUserAction(event) && normalizeSearchValue(searchInput.value).length > 0) { 2064 markSearchSubmitIntent(); 2065 } 2066 }); 2067 } 2068
2069 mobileSearchForms.forEach(function (mobileForm) { 2070 var mobileInput = mobileForm.querySelector("input[name='q']"); 2071 mobileForm.addEventListener("submit", function (event) { 2072 syncSearchCompatInputs(mobileInput ? mobileInput.value : ""); 2073 2074 if ( 2075 isTrustedUserAction(event) && 2076 normalizeSearchValue(mobileInput ? mobileInput.value : "").length > 0 2077 ) { 2078 markSearchSubmitIntent(); 2079 } 2080 }); 2081 }); 2082 2083 mobileToggles.forEach(function (mobileToggle) { 2084 mobileToggle.addEventListener("click", function (e) { 2085 e.preventDefault(); 2086 toggleMobileMenu(); 2087 }); 2088 }); 2089 2090 if (closeBtn) { 2091 closeBtn.addEventListener("click", function () { 2092 closeSearch(); 2093 }); 2094 } 2095 2096 mobileDetails.forEach(function (detail) { 2097 detail.addEventListener("toggle", function () { 2098 var item = detail.closest(".enisa-header__mobile-item"); 2099 if (!item) return; 2100 2101 if (detail.open) { 2102 item.classList.add("is-open"); 2103 } else { 2104 item.classList.remove("is-open"); 2105 } 2106 }); 2107 }); 2108 2109 dropdownToggles.forEach(function (toggle) { 2110 toggle.addEventListener("shown.bs.dropdown", function () { 2111 var parentItem = this.closest("[data-nav-item]"); 2112 if (parentItem) parentItem.classList.add("show"); 2113 this.setAttribute("aria-expanded", "true"); 2114 syncHeaderMetrics(); 2115 syncDropdownState(); 2116 }); 2117 2118 toggle.addEventListener("hidden.bs.dropdown", function () { 2119 var parentItem = this.closest("[data-nav-item]"); 2120 if (parentItem) parentItem.classList.remove("show"); 2121 this.setAttribute("aria-expanded", "false"); 2122 setTimeout(function () { 2123 syncHeaderMetrics(); 2124 syncDropdownState(); 2125 }, 0); 2126 }); 2127 2128 toggle.addEventListener("keydown", function (e) { 2129 var key = e.key; 2130 if ( 2131 key !== "ArrowDown" && 2132 key !== "ArrowUp" && 2133 key !== "Enter" && 2134 key !== " " 2135 ) { 2136 return; 2137 } 2138 2139 var parentItem = toggle.closest("[data-nav-item]"); 2140 var menu = 2141 parentItem && 2142 parentItem.querySelector(".enisa-nav__dropdown.dropdown-menu"); 2143 var links = [].slice.call( 2144 (parentItem || header).querySelectorAll(".enisa-nav__dropdown-link-row") 2145 ); 2146 2147 if (!menu || !links.length) return; 2148 2149 e.preventDefault(); 2150 2151 if (window.bootstrap && window.bootstrap.Dropdown) { 2152 var dd = window.bootstrap.Dropdown.getOrCreateInstance(toggle); 2153 dd.show(); 2154 } 2155 2156 if (key === "ArrowUp") { 2157 safeFocus(links[links.length - 1]); 2158 } else { 2159 safeFocus(links[0]); 2160 } 2161 }); 2162 }); 2163 2164 dropdownMenus.forEach(function (menu) { 2165 menu.addEventListener("keydown", function (e) { 2166 var links = [].slice.call( 2167 menu.querySelectorAll(".enisa-nav__dropdown-link-row") 2168 ); 2169 if (!links.length) return; 2170 2171 var currentIndex = links.indexOf(document.activeElement); 2172 if (currentIndex < 0) return; 2173 2174 if (e.key === "ArrowDown") { 2175 e.preventDefault(); 2176 safeFocus(links[(currentIndex + 1) % links.length]); 2177 return; 2178 } 2179 2180 if (e.key === "ArrowUp") { 2181 e.preventDefault(); 2182 safeFocus(links[(currentIndex - 1 + links.length) % links.length]); 2183 return; 2184 } 2185 2186 if (e.key === "Home") { 2187 e.preventDefault(); 2188 safeFocus(links[0]); 2189 return; 2190 } 2191 2192 if (e.key === "End") { 2193 e.preventDefault(); 2194 safeFocus(links[links.length - 1]); 2195 return; 2196 } 2197 2198 if (e.key === "Escape") { 2199 e.preventDefault(); 2200 closeOpenDropdowns(true); 2201 } 2202 }); 2203 }); 2204 2205 if (overlay) { 2206 overlay.addEventListener("keydown", function (e) { 2207 if (e.key === "ArrowDown" || e.key === "ArrowUp") { 2208 var suggestionActions = getPresearchActions(); 2209 var activeIndex = suggestionActions.indexOf(document.activeElement); 2210 2211 if (activeIndex > -1) { 2212 e.preventDefault(); 2213 var nextIndex = e.key === "ArrowDown" 2214 ? (activeIndex + 1) % suggestionActions.length 2215 : (activeIndex - 1 + suggestionActions.length) % suggestionActions.length; 2216 safeFocus(suggestionActions[nextIndex]); 2217 return; 2218 } 2219 } 2220 2221 if (e.key !== "Tab") return; 2222 2223 var focusable = getFocusableElements(overlay); 2224 2225 if (focusable.length < 2) return; 2226 2227 var first = focusable[0];
2228 var last = focusable[focusable.length - 1]; 2229 2230 if (e.shiftKey) { 2231 if (document.activeElement === first) { 2232 e.preventDefault(); 2233 safeFocus(last); 2234 } 2235 } else { 2236 if (document.activeElement === last) { 2237 e.preventDefault(); 2238 safeFocus(first); 2239 } 2240 } 2241 }); 2242 } 2243 2244 if (mobilePanel) { 2245 mobilePanel.addEventListener("keydown", function (e) { 2246 if (mobilePanel.hidden) return; 2247 if (e.key !== "Tab") return; 2248 2249 var focusable = getFocusableElements(mobilePanel); 2250 2251 if (focusable.length < 2) return; 2252 2253 var first = focusable[0]; 2254 var last = focusable[focusable.length - 1]; 2255 2256 if (e.shiftKey) { 2257 if (document.activeElement === first) { 2258 e.preventDefault(); 2259 safeFocus(last); 2260 } 2261 } else { 2262 if (document.activeElement === last) { 2263 e.preventDefault(); 2264 safeFocus(first); 2265 } 2266 } 2267 }); 2268 } 2269 2270 document.addEventListener("keydown", function (e) { 2271 if (e.key === "Escape") { 2272 var overlayOpen = overlay && !overlay.hidden; 2273 var mobileOpen = mobilePanel && !mobilePanel.hidden; 2274 var hasOpenDropdown = header.classList.contains("has-open-dropdown"); 2275 2276 if (overlayOpen) { 2277 closeSearch(true); 2278 return; 2279 } 2280 2281 if (mobileOpen) { 2282 closeMobileMenu({ restoreFocus: mobileToggles.length > 0 }); 2283 return; 2284 } 2285 2286 if (hasOpenDropdown) { 2287 closeOpenDropdowns(true); 2288 } 2289 } 2290 }); 2291 2292 document.addEventListener("click", function (e) { 2293 if (e.target.closest("[data-history-back]")) { 2294 e.preventDefault(); 2295 history.back(); 2296 return; 2297 } 2298 2299 if (overlay && overlay.getAttribute("aria-hidden") === "false") { 2300 var insideOverlay = overlay.contains(e.target); 2301 var searchToggle = e.target.closest(".js-toggle-search"); 2302 if (!insideOverlay && !searchToggle) { 2303 closeSearch(); 2304 } 2305 } 2306 2307 if (mobilePanel && !mobilePanel.hidden) { 2308 var insideMobilePanel = mobilePanel.contains(e.target); 2309 var mobileMenuToggle = e.target.closest(".js-mobile-menu-toggle"); 2310 if (!insideMobilePanel && !mobileMenuToggle) { 2311 closeMobileMenu(); 2312 } 2313 } 2314 2315 setTimeout(function () { 2316 syncHeaderMetrics(); 2317 syncDropdownState(); 2318 }, 0); 2319 }); 2320 } 2321 2322 if (document.readyState === "loading") { 2323 document.addEventListener("DOMContentLoaded", initHeader); 2324 } else { 2325 initHeader(); 2326 } 2327})(); 2328 2329(function () { 2330 "use strict"; 2331 2332 var SEARCH_FOCUS_INTENT_KEY = "enisa:global-search:focus-results"; 2333 2334 function initGlobalSearchPage() { 2335 var searchInput = document.querySelector("[data-search-input]"); 2336 var suggestionsContainer = document.querySelector("[data-search-suggestions]"); 2337 if (!searchInput || !suggestionsContainer) return; 2338 2339 var form = searchInput.closest("form"); 2340 var clearBtn = form 2341 ? form.querySelector("[data-search-clear]") 2342 : document.querySelector(".enisa-global-search [data-search-clear]"); 2343 var debounceTimer = null; 2344 var reqSeq = 0; 2345 2346 function normalizeVal(v) { 2347 return (v || "").replace(/\s+/g, " ").trim(); 2348 } 2349 2350 function isTrustedUserAction(event) { 2351 return !event || event.isTrusted !== false; 2352 } 2353 2354 function markSearchSubmitIntent() { 2355 try { 2356 window.sessionStorage.setItem(SEARCH_FOCUS_INTENT_KEY, "1"); 2357 } catch (error) {} 2358 } 2359 2360 function updateUi() { 2361 var hasText = normalizeVal(searchInput.value).length > 0; 2362 if (clearBtn) clearBtn.hidden = !hasText; 2363 } 2364 2365 function clearSuggestions() { 2366 suggestionsContainer.innerHTML = ""; 2367 suggestionsContainer.classList.remove("is-visible"); 2368 } 2369 2370 function getSuggestionActions() { 2371 return [].slice.call( 2372 suggestionsContainer.querySelectorAll("a[href], button:not([disabled])") 2373 ).filter(function (element) { 2374 return element.offsetParent !== null; 2375 }); 2376 } 2377
2378 function renderSuggestions(items) { 2379 suggestionsContainer.innerHTML = ""; 2380 2381 if (!items || !items.length) { 2382 suggestionsContainer.classList.remove("is-visible"); 2383 return; 2384 } 2385 2386 var frag = document.createDocumentFragment(); 2387 2388 items.forEach(function (item) { 2389 var name = typeof item === "string" ? item : (item && item.name || ""); 2390 var url = (typeof item === "object" && item) ? (item.url || null) : null; 2391 var normalizedName = normalizeVal(name); 2392 if (!normalizedName) return; 2393 2394 var row = document.createElement("div"); 2395 row.className = "enisa-global-search__suggestion-item"; 2396 row.setAttribute("role", "listitem"); 2397 2398 if (url) { 2399 var link = document.createElement("a"); 2400 link.className = "enisa-global-search__suggestion-btn"; 2401 link.href = url; 2402 link.textContent = normalizedName; 2403 row.appendChild(link); 2404 } else { 2405 var btn = document.createElement("button"); 2406 btn.className = "enisa-global-search__suggestion-btn"; 2407 btn.type = "button"; 2408 btn.textContent = normalizedName; 2409 btn.addEventListener("click", function () { 2410 searchInput.value = normalizedName; 2411 updateUi(); 2412 clearSuggestions(); 2413 markSearchSubmitIntent(); 2414 if (form) form.submit(); 2415 }); 2416 row.appendChild(btn); 2417 } 2418 2419 frag.appendChild(row); 2420 }); 2421 2422 suggestionsContainer.appendChild(frag); 2423 suggestionsContainer.classList.toggle("is-visible", suggestionsContainer.children.length > 0); 2424 } 2425 2426 function fetchSuggestions(term) { 2427 var currentReq = ++reqSeq; 2428 2429 fetch("/buscar/presearch?q=" + encodeURIComponent(term), { 2430 method: "GET", 2431 credentials: "same-origin", 2432 }) 2433 .then(function (res) { 2434 if (!res.ok) throw new Error("presearch failed"); 2435 return res.text(); 2436 }) 2437 .then(function (raw) { 2438 if (currentReq !== reqSeq) return; 2439 2440 var parsed; 2441 try { 2442 parsed = JSON.parse(raw.trim()); 2443 } catch (e) { 2444 parsed = []; 2445 } 2446 2447 var items = Array.isArray(parsed) ? parsed : (parsed.results || parsed.value || []); 2448 renderSuggestions(items); 2449 }) 2450 .catch(function () { 2451 if (currentReq !== reqSeq) return; 2452 clearSuggestions(); 2453 }); 2454 } 2455 2456 searchInput.addEventListener("input", function () { 2457 var val = normalizeVal(searchInput.value); 2458 updateUi(); 2459 clearTimeout(debounceTimer); 2460 2461 if (val.length < 4) { 2462 clearSuggestions(); 2463 return; 2464 } 2465 2466 debounceTimer = setTimeout(function () { 2467 fetchSuggestions(val); 2468 }, 180); 2469 }); 2470 2471 searchInput.addEventListener("keydown", function (event) { 2472 if (event.key === "Escape") { 2473 if (suggestionsContainer.classList.contains("is-visible")) { 2474 event.stopPropagation(); 2475 } 2476 2477 clearSuggestions(); 2478 return; 2479 } 2480 2481 if (event.key === "ArrowDown") { 2482 var actions = getSuggestionActions(); 2483 if (!actions.length) return; 2484 2485 event.preventDefault(); 2486 safeFocus(actions[0]); 2487 } 2488 }); 2489 2490 suggestionsContainer.addEventListener("keydown", function (event) { 2491 var actions = getSuggestionActions(); 2492 var currentIndex = actions.indexOf(document.activeElement); 2493 var nextIndex = currentIndex; 2494 2495 if (currentIndex < 0) return; 2496 2497 if (event.key === "ArrowDown") { 2498 nextIndex = (currentIndex + 1) % actions.length; 2499 } else if (event.key === "ArrowUp") { 2500 nextIndex = (currentIndex - 1 + actions.length) % actions.length; 2501 } else if (event.key === "Home") { 2502 nextIndex = 0; 2503 } else if (event.key === "End") { 2504 nextIndex = actions.length - 1; 2505 } else if (event.key === "Escape") { 2506 event.preventDefault(); 2507 clearSuggestions(); 2508 safeFocus(searchInput); 2509 return; 2510 } else { 2511 return; 2512 } 2513 2514 event.preventDefault(); 2515 safeFocus(actions[nextIndex]); 2516 }); 2517 2518 if (clearBtn) { 2519 clearBtn.addEventListener("click", function (e) { 2520 e.preventDefault(); 2521 searchInput.value = ""; 2522 updateUi(); 2523 clearSuggestions(); 2524 safeFocus(searchInput); 2525 }); 2526 } 2527 2528 if (form) { 2529 form.addEventListener("submit", function (event) { 2530 if (isTrustedUserAction(event) && normalizeVal(searchInput.value).length > 0) { 2531 markSearchSubmitIntent(); 2532 } 2533 }); 2534 } 2535 2536 updateUi(); 2537 } 2538 2539 if (document.readyState === "loading") { 2540 document.addEventListener("DOMContentLoaded", initGlobalSearchPage); 2541 } else { 2542 initGlobalSearchPage(); 2543 } 2544})(); 2545 2546/* 2547 ENISA | JS Module 2548 Modulo: 04-footer 2549 Descripcion: Logica del modulo. 2550*/ 2551(function () { 2552 "use strict"; 2553 2554 var FOOTER_ACCORDION_MOBILE_BREAKPOINT = 575.98; 2555 2556 function isMobileViewport() { 2557 return window.innerWidth <= FOOTER_ACCORDION_MOBILE_BREAKPOINT; 2558 } 2559 2560 function initFooterAccordionBehavior() { 2561 var footer = document.querySelector(".enisa-footer"); 2562 if (!footer) return; 2563 if (footer.getAttribute("data-footer-accordion-init") === "true") return; 2564 footer.setAttribute("data-footer-accordion-init", "true"); 2565 2566 var accordions = [].slice.call( 2567 footer.querySelectorAll(".enisa-footer__accordion"), 2568 ); 2569 if (!accordions.length) return; 2570 2571 function setAccordionState(accordion, expanded) { 2572 var summary = accordion.querySelector(".enisa-footer__accordion-summary"); 2573 var panel = accordion.querySelector(".enisa-footer__accordion-panel"); 2574 if (!summary || !panel) return; 2575 2576 accordion.classList.toggle("is-open", expanded); 2577 summary.setAttribute("aria-expanded", expanded ? "true" : "false"); 2578 panel.hidden = !expanded; 2579 } 2580 2581 function toggleAccordion(accordion) { 2582 var summary = accordion.querySelector(".enisa-footer__accordion-summary"); 2583 if (!summary || !isMobileViewport()) return; 2584 2585 var isExpanded = summary.getAttribute("aria-expanded") === "true";
2586 setAccordionState(accordion, !isExpanded); 2587 } 2588 2589 accordions.forEach(function (accordion) { 2590 var summary = accordion.querySelector(".enisa-footer__accordion-summary"); 2591 var panel = accordion.querySelector(".enisa-footer__accordion-panel"); 2592 if (!summary || !panel) return; 2593 2594 summary.addEventListener("click", function () { 2595 toggleAccordion(accordion); 2596 }); 2597 2598 summary.addEventListener("keydown", function (event) { 2599 if (!isMobileViewport()) return; 2600 if (event.key !== "Enter" && event.key !== " ") return; 2601 2602 event.preventDefault(); 2603 toggleAccordion(accordion); 2604 }); 2605 }); 2606 2607 var lastIsMobile = null; 2608 2609 function syncAccordionState(force) { 2610 var mobile = isMobileViewport(); 2611 if (!force && lastIsMobile === mobile) return; 2612 2613 accordions.forEach(function (accordion) { 2614 var summary = accordion.querySelector(".enisa-footer__accordion-summary"); 2615 var panel = accordion.querySelector(".enisa-footer__accordion-panel"); 2616 if (!summary || !panel) return; 2617 2618 if (mobile) { 2619 summary.setAttribute("role", "button"); 2620 summary.setAttribute("aria-controls", panel.id); 2621 summary.setAttribute("tabindex", "0"); 2622 summary.removeAttribute("aria-disabled");
2623 setAccordionState(accordion, false); 2624 return; 2625 } 2626 2627 accordion.classList.add("is-open"); 2628 panel.hidden = false; 2629 summary.removeAttribute("role"); 2630 summary.removeAttribute("aria-controls"); 2631 summary.removeAttribute("aria-expanded"); 2632 summary.removeAttribute("aria-disabled"); 2633 summary.removeAttribute("tabindex"); 2634 }); 2635 2636 lastIsMobile = mobile; 2637 } 2638 2639 syncAccordionState(true); 2640 2641 window.addEventListener("resize", window.ENISA.utils.debounce(function () { 2642 syncAccordionState(false); 2643 }, 150)); 2644 } 2645 2646 if (document.readyState === "loading") { 2647 document.addEventListener("DOMContentLoaded", initFooterAccordionBehavior); 2648 } else { 2649 initFooterAccordionBehavior(); 2650 } 2651})(); 2652 2653(function () { 2654 "use strict"; 2655 2656 var STORAGE_KEY = "enisa_cookie_consent"; 2657 var ROOT_SELECTOR = "[data-cookie-consent]"; 2658 var ACCEPT_SELECTOR = "[data-cookie-accept]"; 2659 var REJECT_SELECTOR = "[data-cookie-reject]"; 2660 var CLOSE_SELECTOR = "[data-cookie-close]"; 2661 var SETTINGS_ACCEPT_SELECTOR = "[data-cookie-settings-accept]"; 2662 var SETTINGS_REJECT_SELECTOR = "[data-cookie-settings-reject]"; 2663 var SETTINGS_STATUS_SELECTOR = "[data-cookie-settings-status]"; 2664 var OPEN_SETTINGS_SELECTOR = "[data-cookie-open-settings]"; 2665 var PENDING_SCRIPT_SELECTOR = "script[type='text/plain'][data-cookie-category]"; 2666 var NON_ESSENTIAL_CATEGORIES = ["analytics", "marketing", "preferences", "third-party"]; 2667 2668 var NON_ESSENTIAL_COOKIES = [ 2669 "_ga", 2670 "_gid", 2671 "_gat", 2672 "_gcl_au", 2673 "_fbp", 2674 "_clck", 2675 "_clsk" 2676 ]; 2677 2678 function readConsent() { 2679 try { 2680 var raw = window.localStorage.getItem(STORAGE_KEY); 2681 return raw ? JSON.parse(raw) : null; 2682 } catch (error) { 2683 return null; 2684 } 2685 } 2686 2687 function writeConsent(status, version) { 2688 var payload = { 2689 status: status, 2690 version: version, 2691 date: new Date().toISOString() 2692 }; 2693 2694 try { 2695 window.localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); 2696 } catch (error) {} 2697 2698 return payload; 2699 } 2700 2701 function removeConsent() { 2702 try { 2703 window.localStorage.removeItem(STORAGE_KEY); 2704 } catch (error) {} 2705 } 2706 2707 function getCurrentVersion() { 2708 var root = document.querySelector(ROOT_SELECTOR); 2709 return root ? root.getAttribute("data-consent-version") || "1" : "1"; 2710 } 2711 2712 function hasValidConsent(version) { 2713 var consent = readConsent(); 2714 2715 return Boolean( 2716 consent && 2717 consent.version === version && 2718 (consent.status === "accepted" || consent.status === "rejected") 2719 ); 2720 } 2721 2722 function isAccepted(version) { 2723 var consent = readConsent(); 2724 2725 return Boolean( 2726 consent && 2727 consent.version === version && 2728 consent.status === "accepted" 2729 ); 2730 } 2731 2732 function activatePendingScripts() { 2733 var scripts = Array.prototype.slice.call( 2734 document.querySelectorAll(PENDING_SCRIPT_SELECTOR) 2735 ); 2736 2737 scripts.forEach(function (placeholder) { 2738 var category = placeholder.getAttribute("data-cookie-category"); 2739 2740 if (NON_ESSENTIAL_CATEGORIES.indexOf(category) === -1) { 2741 return; 2742 } 2743 2744 var script = document.createElement("script"); 2745 var src = placeholder.getAttribute("data-src"); 2746 2747 if (src) { 2748 script.src = src; 2749 script.async = placeholder.hasAttribute("data-async"); 2750 script.defer = placeholder.hasAttribute("data-defer"); 2751 } else { 2752 script.text = placeholder.textContent || ""; 2753 } 2754 2755 Array.prototype.slice.call(placeholder.attributes).forEach(function (attr) { 2756 if (attr.name.indexOf("data-") === 0 || attr.name === "type") { 2757 return; 2758 } 2759 2760 script.setAttribute(attr.name, attr.value); 2761 }); 2762 2763 placeholder.parentNode.insertBefore(script, placeholder); 2764 placeholder.parentNode.removeChild(placeholder); 2765 }); 2766 } 2767 2768 function deleteCookie(name) { 2769 var hostname = window.location.hostname; 2770 var domainParts = hostname.split("."); 2771 var domains = [hostname]; 2772 var path = "/"; 2773 2774 function expireCookie(value) { 2775 try { 2776 document.cookie = value; 2777 } catch (error) {} 2778 } 2779 2780 if (hostname.indexOf(".") !== -1) { 2781 domains.push("." + hostname); 2782 2783 if (domainParts.length > 2) { 2784 domains.push("." + domainParts.slice(-2).join(".")); 2785 } 2786 } 2787 2788 domains.forEach(function (domain) { 2789 expireCookie( 2790 name + 2791 "=; Max-Age=0; path=" + 2792 path + 2793 "; domain=" + 2794 domain + 2795 "; SameSite=Lax" 2796 ); 2797 2798 expireCookie( 2799 name + 2800 "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=" + 2801 path + 2802 "; domain=" + 2803 domain + 2804 "; SameSite=Lax" 2805 ); 2806 }); 2807 2808 expireCookie( 2809 name + 2810 "=; Max-Age=0; path=" + 2811 path + 2812 "; SameSite=Lax" 2813 ); 2814 2815 expireCookie( 2816 name + 2817 "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=" + 2818 path + 2819 "; SameSite=Lax" 2820 ); 2821 } 2822 2823 function clearKnownNonEssentialCookies() { 2824 NON_ESSENTIAL_COOKIES.forEach(deleteCookie); 2825 } 2826 2827 function getFocusableElements(root) { 2828 if ( 2829 window.ENISA && 2830 window.ENISA.utils && 2831 typeof window.ENISA.utils.getFocusableElements === "function" 2832 ) { 2833 return window.ENISA.utils.getFocusableElements(root); 2834 } 2835 2836 return Array.prototype.slice.call( 2837 root.querySelectorAll( 2838 "a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not
2838([tabindex='-1'])" 2839 ) 2840 ).filter(function (element) { 2841 return element.offsetParent !== null || element === document.activeElement; 2842 }); 2843 } 2844 2845 function safeFocus(element) { 2846 if ( 2847 window.ENISA && 2848 window.ENISA.utils && 2849 typeof window.ENISA.utils.safeFocus === "function" 2850 ) { 2851 return window.ENISA.utils.safeFocus(element); 2852 } 2853 2854 if (!element || typeof element.focus !== "function") return false; 2855 2856 try { 2857 element.focus({ preventScroll: true }); 2858 } catch (error) { 2859 element.focus(); 2860 } 2861 2862 return true; 2863 } 2864 2865 function focusInitialElement(root) { 2866 var focusableElements = getFocusableElements(root); 2867 var target = focusableElements[0] || root; 2868 2869 if (target === root && !root.hasAttribute("tabindex")) { 2870 root.setAttribute("tabindex", "-1"); 2871 } 2872 2873 safeFocus(target); 2874 } 2875 2876 function trapFocus(root, event) { 2877 if (!root || root.hidden || event.key !== "Tab") { 2878 return; 2879 } 2880 2881 var focusableElements = getFocusableElements(root); 2882 2883 if (!focusableElements.length) { 2884 event.preventDefault(); 2885 safeFocus(root); 2886 return; 2887 } 2888 2889 var firstElement = focusableElements[0]; 2890 var lastElement = focusableElements[focusableElements.length - 1]; 2891 2892 if (!root.contains(document.activeElement)) { 2893 event.preventDefault(); 2894 safeFocus(event.shiftKey ? lastElement : firstElement); 2895 return; 2896 } 2897 2898 if (event.shiftKey && document.activeElement === firstElement) { 2899 event.preventDefault(); 2900 safeFocus(lastElement); 2901 return; 2902 } 2903 2904 if (!event.shiftKey && document.activeElement === lastElement) { 2905 event.preventDefault(); 2906 safeFocus(firstElement); 2907 } 2908 } 2909 2910 var resizeHandler = null; 2911 var activeFocusTrapRoot = null; 2912 var isRedirectingFocus = false; 2913 var bgLockScrollY = 0; 2914 var bgLockScrollX = 0; 2915 var lastSettingsTrigger = null; 2916 var focusBeforeBanner = null; 2917 var BG_LOCK_ATTR = "data-cookie-bg-hidden"; 2918 var BG_SELECTORS = ".enisa-skip-link, #main-content, .page-layout__main, [data-header-root], .enisa-header, footer, .enisa-footer"; 2919 2920 function getFocusTrapFallback(root, useLast) { 2921 var focusableElements = getFocusableElements(root); 2922 return focusableElements[useLast ? focusableElements.length - 1 : 0] || root; 2923 } 2924 2925 function handleDocumentFocusTrapKeydown(event) { 2926 if (!activeFocusTrapRoot) return; 2927 trapFocus(activeFocusTrapRoot, event); 2928 } 2929 2930 function handleDocumentFocusTrapFocusin(event) { 2931 if ( 2932 !activeFocusTrapRoot || 2933 isRedirectingFocus || 2934 activeFocusTrapRoot.hidden || 2935 activeFocusTrapRoot.contains(event.target) 2936 ) { 2937 return; 2938 } 2939 2940 isRedirectingFocus = true; 2941 2942 try { 2943 safeFocus(getFocusTrapFallback(activeFocusTrapRoot, false)); 2944 } finally { 2945 isRedirectingFocus = false; 2946 } 2947 } 2948 2949 function enableFocusTrap(root) { 2950 if (activeFocusTrapRoot === root) return; 2951 2952 activeFocusTrapRoot = root; 2953 document.addEventListener("keydown", handleDocumentFocusTrapKeydown, true); 2954 document.addEventListener("focusin", handleDocumentFocusTrapFocusin, true); 2955 } 2956 2957 function disableFocusTrap(root) { 2958 if (activeFocusTrapRoot !== root) return; 2959 2960 document.removeEventListener("keydown", handleDocumentFocusTrapKeydown, true); 2961 document.removeEventListener("focusin", handleDocumentFocusTrapFocusin, true); 2962 activeFocusTrapRoot = null; 2963 } 2964 2965 function canRestoreFocus(element, root) { 2966 if (!element || typeof element.focus !== "function") return false; 2967 if (root && root.contains(element)) return false; 2968 if (!document.documentElement.contains(element)) return false; 2969 2970 if ( 2971 window.ENISA && 2972 window.ENISA.utils && 2973 typeof window.ENISA.utils.isElementVisible === "function" 2974 ) { 2975 return window.ENISA.utils.isElementVisible(element); 2976 } 2977 2978 return !element.hidden && element.offsetParent !== null; 2979 } 2980 2981 function releaseFocus(root) { 2982 var target = null; 2983 2984 if (!root || !root.contains(document.activeElement)) return; 2985 2986 if (canRestoreFocus(lastSettingsTrigger, root)) { 2987 target = lastSettingsTrigger; 2988 } else if (canRestoreFocus(focusBeforeBanner, root)) { 2989 target = focusBeforeBanner; 2990 } 2991 2992 if (!target) { 2993 target = document.getElementById("main-content"); 2994 } 2995 2996 if (target) { 2997 safeFocus(target); 2998 } 2999 } 3000 3001 function setElementInert(element, isInert) { 3002 if ( 3003 window.ENISA && 3004 window.ENISA.utils && 3005 typeof window.ENISA.utils.setInert === "function" 3006 ) { 3007 window.ENISA.utils.setInert(element, isInert); 3008 return; 3009 } 3010 3011 if (isInert) { 3012 element.setAttribute("aria-hidden", "true"); 3013 if ("inert" in element) element.inert = true; 3014 return; 3015 } 3016 3017 element.removeAttribute("aria-hidden"); 3018 if ("inert" in element) element.inert = false;
3019 } 3020 3021 function lockBackground(root) { 3022 [].slice.call(document.querySelectorAll(BG_SELECTORS)).forEach(function (el) { 3023 if (!el || el === root || root.contains(el) || el.contains(root)) return; 3024 el.setAttribute(BG_LOCK_ATTR, el.getAttribute("aria-hidden") || ""); 3025 setElementInert(el, true); 3026 }); 3027 3028 bgLockScrollY = window.scrollY || window.pageYOffset || 0; 3029 bgLockScrollX = window.scrollX || window.pageXOffset || 0; 3030 document.body.style.position = "fixed"; 3031 document.body.style.top = "-" + bgLockScrollY + "px"; 3032 document.body.style.width = "100%"; 3033 } 3034 3035 function unlockBackground() { 3036 [].slice.call(document.querySelectorAll("[" + BG_LOCK_ATTR + "]")).forEach(function (el) { 3037 var prev = el.getAttribute(BG_LOCK_ATTR); 3038 el.removeAttribute(BG_LOCK_ATTR); 3039 setElementInert(el, false); 3040 if (prev) el.setAttribute("aria-hidden", prev); 3041 }); 3042 3043 document.body.style.position = ""; 3044 document.body.style.top = ""; 3045 document.body.style.width = ""; 3046 if ( 3047 window.ENISA && 3048 window.ENISA.utils && 3049 typeof window.ENISA.utils.restoreScrollPosition === "function" 3050 ) { 3051 window.ENISA.utils.restoreScrollPosition(bgLockScrollY, bgLockScrollX); 3052 } else { 3053 window.scrollTo(bgLockScrollX, bgLockScrollY); 3054 } 3055 } 3056 3057 function syncCookieOffset(root) { 3058 if (!root) return; 3059 3060 var height = root.getBoundingClientRect().height; 3061 var safeBottom = 0; 3062 3063 try { 3064 var style = window.getComputedStyle(document.documentElement); 3065 var raw = style.getPropertyValue("--enisa-safe-area-bottom").trim(); 3066 safeBottom = parseFloat(raw) || 0; 3067 } catch (error) {} 3068 3069 document.documentElement.style.setProperty( 3070 "--enisa-cookie-banner-offset", 3071 height + 32 + safeBottom + "px" 3072 ); 3073 } 3074 3075 function clearCookieOffset() { 3076 document.documentElement.style.removeProperty("--enisa-cookie-banner-offset"); 3077 } 3078 3079 function showBanner(root) { 3080 if (!root) return; 3081 3082 focusBeforeBanner = document.activeElement && !root.contains(document.activeElement) 3083 ? document.activeElement 3084 : null; 3085 3086 root.hidden = false; 3087 root.removeAttribute("hidden"); 3088 root.removeAttribute("aria-hidden"); 3089 document.body.classList.add("cookie-consent-is-open"); 3090 3091 lockBackground(root); 3092 enableFocusTrap(root); 3093 focusInitialElement(root); 3094 syncCookieOffset(root); 3095 3096 requestAnimationFrame(function () { 3097 requestAnimationFrame(function () { 3098 syncCookieOffset(root); 3099 }); 3100 }); 3101 3102 if (!resizeHandler) { 3103 resizeHandler = function () { 3104 if (!root.hidden) { 3105 syncCookieOffset(root); 3106 } 3107 }; 3108 3109 window.addEventListener("resize", resizeHandler, { passive: true }); 3110 window.addEventListener("orientationchange", resizeHandler); 3111 } 3112 } 3113 3114 function hideBanner(root) { 3115 if (!root) return; 3116 3117 document.body.classList.remove("cookie-consent-is-open"); 3118 clearCookieOffset(); 3119 disableFocusTrap(root); 3120 unlockBackground(); 3121 3122 if (resizeHandler) { 3123 window.removeEventListener("resize", resizeHandler); 3124 window.removeEventListener("orientationchange", resizeHandler); 3125 resizeHandler = null; 3126 } 3127 3128 releaseFocus(root); 3129 lastSettingsTrigger = null; 3130 focusBeforeBanner = null; 3131 3132 root.hidden = true; 3133 root.setAttribute("hidden", ""); 3134 root.setAttribute("aria-hidden", "true"); 3135 } 3136 3137 function updateSettingsStatus(status) { 3138 var statusNodes = Array.prototype.slice.call( 3139 document.querySelectorAll(SETTINGS_STATUS_SELECTOR) 3140 ); 3141 3142 if (!statusNodes.length) { 3143 return; 3144 } 3145 3146 var message = "Puedes modificar tu consentimiento en cualquier momento."; 3147 3148 if (status === "accepted") { 3149 message = "Has aceptado las cookies analÃticas."; 3150 } 3151 3152 if (status === "rejected") { 3153 message = "Has rechazado las cookies analÃticas."; 3154 } 3155 3156 statusNodes.forEach(function (node) { 3157 node.textContent = message; 3158 }); 3159 } 3160 3161 function acceptCookies(version) { 3162 var root = document.querySelector(ROOT_SELECTOR); 3163 var activeVersion = version || getCurrentVersion(); 3164 3165 writeConsent("accepted", activeVersion); 3166 activatePendingScripts(); 3167 updateSettingsStatus("accepted"); 3168 hideBanner(root); 3169 } 3170 3171 function rejectCookies(version) { 3172 var root = document.querySelector(ROOT_SELECTOR); 3173 var activeVersion = version || getCurrentVersion(); 3174 3175 writeConsent("rejected", activeVersion); 3176 clearKnownNonEssentialCookies(); 3177 updateSettingsStatus("rejected"); 3178 hideBanner(root); 3179 } 3180 3181 function openCookieSettings(trigger) { 3182 var root = document.querySelector(ROOT_SELECTOR); 3183 3184 lastSettingsTrigger = trigger || document.activeElement || null; 3185 removeConsent(); 3186 clearKnownNonEssentialCookies(); 3187 updateSettingsStatus(null); 3188 showBanner(root); 3189 } 3190 3191 function bindCookieSettingsTrigger() { 3192 document.addEventListener("click", function (event) { 3193 var trigger = event.target.closest(OPEN_SETTINGS_SELECTOR); 3194 3195 if (!trigger) { 3196 return; 3197 } 3198 3199 event.preventDefault(); 3200 openCookieSettings(trigger); 3201 }); 3202 } 3203 3204 function bindSettingsPageActions() { 3205 document.addEventListener("click", function (event) { 3206 var acceptTrigger = event.target.closest(SETTINGS_ACCEPT_SELECTOR); 3207 var rejectTrigger = event.target.closest(SETTINGS_REJECT_SELECTOR); 3208 3209 if (acceptTrigger) { 3210 event.preventDefault(); 3211 acceptCookies(getCurrentVersion()); 3212 return; 3213 } 3214 3215 if (rejectTrigger) { 3216 event.preventDefault(); 3217 rejectCookies(getCurrentVersion()); 3218 } 3219 }); 3220 } 3221 3222 function bindBannerActions(root, version) { 3223 if (!root || root.getAttribute("data-cookie-banner-actions-init") === "true") { 3224 return; 3225 } 3226 3227 root.setAttribute("data-cookie-banner-actions-init", "true"); 3228 3229 root.addEventListener("click", function (event) { 3230 if (event.target.closest(ACCEPT_SELECTOR)) { 3231 event.preventDefault(); 3232 acceptCookies(version); 3233 return; 3234 } 3235 3236 if (event.target.closest(REJECT_SELECTOR)) { 3237 event.preventDefault(); 3238 rejectCookies(version); 3239 return; 3240 } 3241 3242 if (event.target.closest(CLOSE_SELECTOR)) { 3243 event.preventDefault(); 3244 rejectCookies(version); 3245 } 3246 }); 3247 3248 root.addEventListener("keydown", function (event) { 3249 if (event.key === "Escape" || event.key === "Esc") { 3250 event.preventDefault(); 3251 rejectCookies(version); 3252 return; 3253 } 3254 3255 trapFocus(root, event); 3256 }); 3257 } 3258 3259 function syncInitialSettingsStatus(version) { 3260 var consent = readConsent(); 3261 3262 if (!consent || consent.version !== version) { 3263 updateSettingsStatus(null); 3264 return; 3265 } 3266 3267 updateSettingsStatus(consent.status); 3268 } 3269 3270 function initCookieConsent() { 3271 var root = document.querySelector(ROOT_SELECTOR); 3272 var version = root ? root.getAttribute("data-consent-version") || "1" : "1"; 3273 3274 syncInitialSettingsStatus(version); 3275 3276 if (!root) { 3277 return; 3278 } 3279 3280 bindBannerActions(root, version); 3281 3282 if (root.getAttribute("data-cookie-consent-init") === "true") { 3283 if (isAccepted(version)) { 3284 activatePendingScripts(); 3285 } 3286 3287 return; 3288 } 3289 3290 root.setAttribute("data-cookie-consent-init", "true"); 3291 3292 if (isAccepted(version)) { 3293 activatePendingScripts(); 3294 return; 3295 } 3296 3297 if (!hasValidConsent(version)) { 3298 showBanner(root); 3299 } 3300 } 3301 3302 bindCookieSettingsTrigger(); 3303 bindSettingsPageActions(); 3304 3305 window.ENISA = window.ENISA || {}; 3306 window.ENISA.cookies = { 3307 readConsent: readConsent, 3308 accept: acceptCookies, 3309 reject: rejectCookies, 3310 openSettings: openCookieSettings, 3311 clearKnownNonEssentialCookies: clearKnownNonEssentialCookies 3312 }; 3313 3314 if (document.readyState === "loading") { 3315 document.addEventListener("DOMContentLoaded", initCookieConsent); 3316 } else {
3317 initCookieConsent(); 3318 } 3319})(); 3320 3321(function () { 3322 "use strict"; 3323 3324 var INIT_ATTR = "data-enisa-external-web-link-init"; 3325 var SKIP_ATTR = "data-skip-external-link"; 3326 var MODULE_NAMESPACE = "externalWebLinks"; 3327 var OBSERVER_DEBOUNCE = 150; 3328 3329 function isElement(node) { 3330 return node && node.nodeType === 1; 3331 } 3332 3333 function isWebHref(href) { 3334 return /^https?:\/\//i.test((href || "").trim()); 3335 } 3336 3337 function normalizeOrigin(origin) { 3338 return (origin || "").replace(/\/$/, "").toLowerCase(); 3339 } 3340 3341 function isSameOrigin(url) { 3342 return ( 3343 normalizeOrigin(url.origin) === normalizeOrigin(window.location.origin) 3344 ); 3345 } 3346 3347 function shouldSkipLink(link) { 3348 if (!link || link.hasAttribute(SKIP_ATTR)) { 3349 return true; 3350 } 3351 3352 var href = link.getAttribute("href"); 3353 3354 if (!isWebHref(href)) { 3355 return true; 3356 } 3357 3358 var url; 3359 3360 try { 3361 url = new URL(href); 3362 } catch (error) { 3363 return true; 3364 } 3365 3366 return isSameOrigin(url); 3367 } 3368 3369 function mergeRel(link) { 3370 var relValues = (link.getAttribute("rel") || "") 3371 .split(/\s+/) 3372 .map(function (value) { 3373 return value.trim().toLowerCase(); 3374 }) 3375 .filter(Boolean); 3376 3377 if (relValues.indexOf("noopener") === -1) { 3378 relValues.push("noopener"); 3379 } 3380 3381 if (relValues.indexOf("noreferrer") === -1) { 3382 relValues.push("noreferrer"); 3383 } 3384 3385 link.setAttribute("rel", relValues.join(" ")); 3386 } 3387 3388 function normalizeText(value) { 3389 return String(value || "") 3390 .replace(/\s+/g, " ") 3391 .trim() 3392 .toLowerCase() 3393 .replace(/[áà äâ]/g, "a") 3394 .replace(/[éèëê]/g, "e") 3395 .replace(/[Ãìïî]/g, "i") 3396 .replace(/[óòöô]/g, "o") 3397 .replace(/[úùüû]/g, "u") 3398 .replace(/ñ/g, "n"); 3399 } 3400 3401 function hasNewWindowAnnouncement(value) { 3402 var text = normalizeText(value); 3403 3404 return ( 3405 text.indexOf("nueva ventana") !== -1 || 3406 text.indexOf("nueva pestana") !== -1 || 3407 text.indexOf("pestana nueva") !== -1 3408 ); 3409 } 3410 3411 function addNewTabAnnouncement(link) { 3412 var alreadyHas = link.querySelector("[data-new-tab-hint], [data-new-window-hint]"); 3413 if (alreadyHas) return; 3414 if (hasNewWindowAnnouncement(link.getAttribute("aria-label"))) return; 3415 if (hasNewWindowAnnouncement(link.getAttribute("title"))) return; 3416 if (hasNewWindowAnnouncement(link.textContent)) return; 3417 3418 var hint = document.createElement("span"); 3419 hint.setAttribute("data-new-tab-hint", ""); 3420 hint.className = "enisa-sr-only"; 3421 hint.textContent = " (se abre en una nueva ventana)"; 3422 link.appendChild(hint); 3423 } 3424 3425 function enhanceLink(link) { 3426 if (!link || link.getAttribute(INIT_ATTR) === "true") { 3427 return; 3428 } 3429 3430 link.setAttribute(INIT_ATTR, "true"); 3431 3432 if (shouldSkipLink(link)) { 3433 return; 3434 } 3435 3436 if (!link.hasAttribute("target")) { 3437 link.setAttribute("target", "_blank"); 3438 } 3439 3440 mergeRel(link); 3441 addNewTabAnnouncement(link); 3442 } 3443 3444 function getLinksFromScope(scope) { 3445 var root = scope || document; 3446 3447 if (!root.querySelectorAll) { 3448 return []; 3449 } 3450 3451 return Array.prototype.slice.call( 3452 root.querySelectorAll("a[href]:not([" + INIT_ATTR + "])"), 3453 ); 3454 } 3455 3456 function initExternalWebLinks(scope) { 3457 var links = getLinksFromScope(scope); 3458 3459 links.forEach(function (link) { 3460 enhanceLink(link); 3461 }); 3462 } 3463 3464 function nodeHasLinks(node) { 3465 if (!isElement(node)) { 3466 return false; 3467 } 3468 3469 if (node.matches && node.matches("a[href]")) { 3470 return true; 3471 } 3472 3473 return !!(node.querySelector && node.querySelector("a[href]")); 3474 } 3475 3476 function initMutationObserver() { 3477 if (!("MutationObserver" in window) || !document.body) { 3478 return; 3479 } 3480 3481 var timer = null; 3482 3483 var observer = new MutationObserver(function (mutations) { 3484 var hasNewLinks = mutations.some(function (mutation) { 3485 return Array.prototype.slice 3486 .call(mutation.addedNodes) 3487 .some(nodeHasLinks); 3488 }); 3489 3490 if (!hasNewLinks) { 3491 return; 3492 } 3493 3494 clearTimeout(timer); 3495 3496 timer = setTimeout(function () {
3497 initExternalWebLinks(document); 3498 }, OBSERVER_DEBOUNCE); 3499 }); 3500 3501 observer.observe(document.body, { 3502 childList: true, 3503 subtree: true, 3504 }); 3505 } 3506 3507 function init() { 3508 initExternalWebLinks(document); 3509 initMutationObserver(); 3510 } 3511 3512 if (document.readyState === "loading") { 3513 document.addEventListener("DOMContentLoaded", init); 3514 } else { 3515 init(); 3516 } 3517 3518 window.ENISA = window.ENISA || {}; 3519 window.ENISA[MODULE_NAMESPACE] = { 3520 init: initExternalWebLinks, 3521 }; 3522})(); 3523 3524/* 3525 ENISA | JS Module 3526 Modulo: 07-card-links 3527 Descripcion: Hace foco y activacion por teclado sobre la card, usando un enlace primario interno como destino. 3528*/ 3529(function () { 3530 "use strict"; 3531 3532 var ROOT_SELECTOR = "[data-card-link]"; 3533 var TARGET_SELECTOR = "[data-card-link-target]"; 3534 var INIT_ATTR = "data-card-link-init"; 3535 var FOCUS_CLASS = "is-card-focus-visible"; 3536 var INTERACTIVE_SELECTOR = 3537 "a[href], button, input, select, textarea, summary, [role='button'], [role='link']"; 3538 3539 function getTarget(root) { 3540 return root ? root.querySelector(TARGET_SELECTOR) : null; 3541 } 3542 3543 function isModifiedEvent(event) { 3544 return !!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0); 3545 } 3546 3547 function isInteractiveDescendant(target, root) { 3548 if (!target || !root || target === root || !target.closest) return false; 3549 3550 var interactive = target.closest(INTERACTIVE_SELECTOR); 3551 return !!(interactive && root.contains(interactive) && interactive !== root); 3552 } 3553 3554 function activateTarget(target) { 3555 if (!target) return; 3556 target.click(); 3557 } 3558 3559 function isKeyboardFocus() { 3560 return document.documentElement.classList.contains("enisa-using-keyboard"); 3561 } 3562 3563 function isTargetFocus(eventTarget, root) { 3564 if (!eventTarget || !root || !eventTarget.matches) return false; 3565 return eventTarget.matches(TARGET_SELECTOR) && root.contains(eventTarget); 3566 } 3567 3568 function isVisibleFocus(eventTarget) { 3569 if (!eventTarget || !eventTarget.matches) return false; 3570 3571 try { 3572 return eventTarget.matches(":focus-visible"); 3573 } catch (error) { 3574 return false; 3575 } 3576 } 3577 3578 function initRoot(root) { 3579 var target; 3580 3581 if (!root || root.getAttribute(INIT_ATTR) === "true") return; 3582 3583 target = getTarget(root); 3584 if (!target) return; 3585 3586 root.setAttribute(INIT_ATTR, "true"); 3587 3588 root.addEventListener("click", function (event) { 3589 if (isModifiedEvent(event) || isInteractiveDescendant(event.target, root)) return; 3590 activateTarget(target); 3591 }); 3592 3593 root.addEventListener("focusin", function (event) { 3594 if (isTargetFocus(event.target, root) && (isKeyboardFocus() || isVisibleFocus(event.target))) { 3595 root.classList.add(FOCUS_CLASS); 3596 } 3597 }); 3598 3599 root.addEventListener("focusout", function (event) { 3600 if (!root.contains(event.relatedTarget)) { 3601 root.classList.remove(FOCUS_CLASS); 3602 } 3603 }); 3604 } 3605 3606 function toArray(list) { 3607 return Array.prototype.slice.call(list || []); 3608 } 3609 3610 function initScope(scope) { 3611 var roots = []; 3612 3613 if (!scope || !scope.querySelectorAll) return; 3614 3615 if (scope.matches && scope.matches(ROOT_SELECTOR)) { 3616 roots.push(scope); 3617 } 3618 3619 roots = roots.concat(toArray(scope.querySelectorAll(ROOT_SELECTOR))); 3620 roots.forEach(initRoot); 3621 } 3622 3623 function init() { 3624 initScope(document); 3625 3626 if (!("MutationObserver" in window) || !document.body) return; 3627 3628 var observer = new MutationObserver(function (mutations) { 3629 mutations.forEach(function (mutation) { 3630 toArray(mutation.addedNodes).forEach(function (node) { 3631 if (node && node.nodeType === 1) initScope(node); 3632 }); 3633 }); 3634 }); 3635 3636 observer.observe(document.body, { 3637 childList: true, 3638 subtree: true 3639 }); 3640 } 3641 3642 if (document.readyState === "loading") { 3643 document.addEventListener("DOMContentLoaded", init); 3644 } else { 3645 init(); 3646 } 3647})(); 3648 3649/* 3650 ENISA | JS Module 3651 Modulo: 08-hero 3652 Descripcion: Secuencia de entrada de heroes y señal global para sincronizar reveal. 3653*/ 3654(function () { 3655 "use strict"; 3656 3657 var HERO_SELECTOR = 3658 "[data-hero-sequence], .enisa-hero, .enisa-page-hero, .enisa-hero-simple, .enisa-editorial-hero, .enisa-404"; 3659 3660 var HERO_READY_CLASS = "is-hero-ready"; 3661 var HERO_ANIMATED_CLASS = "is-animated"; 3662 var HERO_READY_EVENT = "enisa:hero:ready"; 3663 var FIRST_HERO_READY_ATTR = "data-enisa-first-hero-ready"; 3664 3665 function prefersReducedMotion() { 3666 return window.matchMedia && 3667 window.matchMedia("(prefers-reduced-motion: reduce)").matches; 3668 } 3669 3670 function getFirstVisibleHero(heroes) {
3671 var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; 3672 3673 return heroes.find(function (hero) { 3674 if (!hero || !hero.getClientRects || !hero.getClientRects().length) return false; 3675 3676 var rect = hero.getBoundingClientRect(); 3677 return rect.bottom > 0 && rect.top < viewportHeight; 3678 }) || heroes[0] || null; 3679 } 3680 3681 function getHeroReadyDelay(hero) { 3682 if (!hero) return 0; 3683 3684 var fromAttr = parseInt(hero.getAttribute("data-hero-ready-ms") || "", 10); 3685 if (!isNaN(fromAttr) && fromAttr >= 0) return fromAttr; 3686 3687 var fromCSS = parseFloat( 3688 window.getComputedStyle(hero).getPropertyValue("--enisa-hero-ready-ms") || "" 3689 ); 3690 if (!isNaN(fromCSS) && fromCSS >= 0) return fromCSS; 3691 3692 if (hero.matches(".enisa-hero--slider")) return 980; 3693 if (hero.matches(".enisa-page-hero")) return 760; 3694 if (hero.matches(".enisa-hero-simple")) return 880; 3695 if (hero.matches(".enisa-editorial-hero")) return 780; 3696 if (hero.matches(".enisa-hero")) return 980; 3697 3698 return 700; 3699 } 3700 3701 function dispatchHeroReady(hero) { 3702 if (!hero || hero.classList.contains(HERO_READY_CLASS)) return; 3703 3704 hero.classList.add(HERO_READY_CLASS); 3705 3706 if (document.documentElement.getAttribute(FIRST_HERO_READY_ATTR) !== "true") { 3707 document.documentElement.setAttribute(FIRST_HERO_READY_ATTR, "true"); 3708 3709 document.dispatchEvent(new CustomEvent(HERO_READY_EVENT, { 3710 detail: { 3711 hero: hero 3712 } 3713 })); 3714 } 3715 } 3716 3717 function animateHero(hero, isFirstHero) { 3718 if (!hero) return; 3719 3720 if (hero.classList.contains(HERO_ANIMATED_CLASS)) { 3721 if (isFirstHero) { 3722 window.setTimeout(function () { 3723 dispatchHeroReady(hero); 3724 }, getHeroReadyDelay(hero)); 3725 } 3726 return; 3727 } 3728 3729 hero.classList.add(HERO_ANIMATED_CLASS); 3730 3731 if (isFirstHero) { 3732 window.setTimeout(function () { 3733 dispatchHeroReady(hero); 3734 }, getHeroReadyDelay(hero)); 3735 } 3736 } 3737 3738 function initHeroSequence() { 3739 var heroes = [].slice.call(document.querySelectorAll(HERO_SELECTOR)); 3740 if (!heroes.length) { 3741 document.documentElement.setAttribute(FIRST_HERO_READY_ATTR, "true"); 3742 document.dispatchEvent(new CustomEvent(HERO_READY_EVENT, { detail: { hero: null } })); 3743 return; 3744 } 3745 3746 var firstHero = getFirstVisibleHero(heroes); 3747 3748 if (prefersReducedMotion() || !("IntersectionObserver" in window)) { 3749 heroes.forEach(function (hero) { 3750 hero.classList.add(HERO_ANIMATED_CLASS); 3751 }); 3752 dispatchHeroReady(firstHero); 3753 return; 3754 } 3755 3756 var io = new IntersectionObserver( 3757 function (entries) { 3758 entries.forEach(function (entry) { 3759 if (!entry.isIntersecting) return; 3760 3761 animateHero(entry.target, entry.target === firstHero); 3762 io.unobserve(entry.target); 3763 }); 3764 }, 3765 { 3766 threshold: 0, 3767 rootMargin: "0px 0px -8% 0px" 3768 } 3769 ); 3770 3771 heroes.forEach(function (hero) { 3772 var rect = hero.getBoundingClientRect(); 3773 3774 if (rect.top < window.innerHeight) { 3775 animateHero(hero, hero === firstHero); 3776 return; 3777 } 3778 3779 io.observe(hero); 3780 }); 3781 } 3782 3783 if (document.readyState === "loading") { 3784 document.addEventListener("DOMContentLoaded", initHeroSequence); 3785 } else { 3786 initHeroSequence(); 3787 } 3788})(); 3789 3790(function () { 3791 "use strict"; 3792 3793 var HERO_SELECTOR = "[data-editorial-hero]"; 3794 var STORAGE_KEY = "enisa_editorial_saved"; 3795 3796 function readSaved() { 3797 try { 3798 var raw = window.localStorage.getItem(STORAGE_KEY); 3799 if (!raw) return []; 3800 var parsed = JSON.parse(raw); 3801 return Array.isArray(parsed) ? parsed : []; 3802 } catch (e) { 3803 return []; 3804 } 3805 } 3806 3807 function writeSaved(list) { 3808 try { 3809 window.localStorage.setItem(STORAGE_KEY, JSON.stringify(list)); 3810 } catch (e) {} 3811 } 3812 3813 function initEditorialHero(root) { 3814 if (!root || root.getAttribute("data-editorial-hero-init") === "true") return; 3815 root.setAttribute("data-editorial-hero-init", "true"); 3816 3817 var shareBtn = root.querySelector("[data-editorial-share]"); 3818 var saveBtn = root.querySelector("[data-editorial-save]"); 3819 var saveLabel = root.querySelector("[data-editorial-save-label]"); 3820 var slug = (root.getAttribute("data-editorial-slug") || "").trim(); 3821 var shareUrl = (root.getAttribute("data-editorial-share-url") || window.location.href || "").trim(); 3822 var titleEl = 3823 root.querySelector(".enisa-editorial-hero__title") || 3824 root.querySelector(".enisa-page-hero__title"); 3825 var shareTitle = titleEl ? titleEl.textContent.trim() : document.title; 3826 3827 function updateSaveState() { 3828 if (!saveBtn || !saveLabel || !slug) return; 3829 var saved = readSaved(); 3830 var isSaved = saved.indexOf(slug) !== -1; 3831 saveBtn.classList.toggle("is-saved", isSaved); 3832 saveBtn.setAttribute("aria-pressed", isSaved ? "true" : "false"); 3833 saveLabel.textContent = isSaved ? "Guardado" : "Guardar"; 3834 } 3835 3836 if (shareBtn) { 3837 shareBtn.addEventListener("click", function () { 3838 if (navigator.share) { 3839 navigator.share({ title: shareTitle, url: shareUrl }).catch(function () {}); 3840 return; 3841 } 3842 3843 if (navigator.clipboard && navigator.clipboard.writeText) { 3844 navigator.clipboard.writeText(shareUrl).then(function () { 3845 shareBtn.classList.add("is-saved"); 3846 setTimeout(function () { 3847 shareBtn.classList.remove("is-saved"); 3848 }, 1200); 3849 }).catch(function () {}); 3850 } 3851 }); 3852 } 3853 3854 if (saveBtn && slug) { 3855 saveBtn.addEventListener("click", function () { 3856 var saved = readSaved(); 3857 var index = saved.indexOf(slug); 3858 if (index === -1) { 3859 saved.push(slug); 3860 } else { 3861 saved.splice(index, 1); 3862 } 3863 writeSaved(saved); 3864 updateSaveState(); 3865 }); 3866 } 3867 3868 updateSaveState(); 3869 } 3870 3871 function normalizeMediaUrl(url) { 3872 var value = (url || "").trim(); 3873 if (!value) return ""; 3874 var noQuery = value.split("?")[0].split("#")[0]; 3875 return noQuery.toLowerCase(); 3876 } 3877 3878 function initProseDedupImages() { 3879 var proseBlocks = [].slice.call(document.querySelectorAll("[data-prose-dedup-image]")); 3880 if (!proseBlocks.length) return; 3881 3882 proseBlocks.forEach(function (block) { 3883 if (!block || block.getAttribute("data-prose-dedup-init") === "true") return; 3884 block.setAttribute("data-prose-dedup-init", "true"); 3885 3886 var target = normalizeMediaUrl(block.getAttribute("data-prose-dedup-image")); 3887 if (!target) return; 3888 3889 var firstDuplicate = null; 3890
3891 [].slice.call(block.querySelectorAll("img")).forEach(function (img) { 3892 if (!img || firstDuplicate) return; 3893 3894 var src = normalizeMediaUrl(img.getAttribute("src") || img.currentSrc || ""); 3895 if (!src || src !== target) return; 3896 3897 firstDuplicate = img; 3898 }); 3899 3900 if (!firstDuplicate) return; 3901 3902 var wrapper = 3903 firstDuplicate.closest("figure, p, div") || 3904 firstDuplicate.parentElement || 3905 firstDuplicate; 3906 3907 if (wrapper && wrapper !== block) { 3908 wrapper.setAttribute("hidden", ""); 3909 } else { 3910 firstDuplicate.setAttribute("hidden", ""); 3911 } 3912 }); 3913 } 3914 3915 function init() { 3916 var heroes = document.querySelectorAll(HERO_SELECTOR); 3917 3918 if (heroes.length) { 3919 [].forEach.call(heroes, initEditorialHero); 3920 } 3921 3922 initProseDedupImages(); 3923 } 3924 3925 if (document.readyState === "loading") { 3926 document.addEventListener("DOMContentLoaded", init); 3927 } else { 3928 init(); 3929 } 3930})(); 3931 3932/* 3933 ENISA | JS Module 3934 Modulo: 09-reveal 3935 Descripcion: Reveal progresivo sincronizado con el primer hero visible. 3936*/ 3937(function () { 3938 "use strict"; 3939 3940 var REVEAL_SELECTOR = ".is-reveal, .is-bg-reveal"; 3941 var VISIBLE_CLASS = "is-visible"; 3942 var INIT_ATTR = "data-reveal-init"; 3943 var REVEAL_GROUP_ATTR = "data-reveal-group"; 3944 var REVEAL_CHILD_INIT_ATTR = "data-reveal-child-init"; 3945 var STAGGER_STEP = 70; 3946 var STAGGER_MAX = 8; 3947 var REVEAL_GAP_MS = 16; 3948 var HERO_READY_EVENT = "enisa:hero:ready"; 3949 var HERO_READY_ATTR = "data-enisa-first-hero-ready"; 3950 var FALLBACK_GATE_MS = 1100; 3951 3952 var SKIP_SELECTOR = 3953 ".enisa-hero, .enisa-page-hero, .enisa-hero-simple, .enisa-editorial-hero, .enisa-404"; 3954 3955 var AUTO_REVEAL_SCOPE = 3956 "#main-content > [class^='enisa-'], #main-content > [class*=' enisa-'], " + 3957 "#main-content > [class^='enisa-'], #main-content > [class*=' enisa-'], " + 3958 ".page-layout__content > [class^='enisa-'], .page-layout__content > [class*=' enisa-'], " + 3959 ".page-layout__main > [class^='enisa-'], .page-layout__main > [class*=' enisa-']"; 3960 3961 var observer = null; 3962 var moTimer = null; 3963 var fallbackTimer = null; 3964 var revealStarted = false; 3965 var MO_SCOPE = document.querySelector(".page-layout__content") || document.body; 3966 var revealQueue = []; 3967 var revealQueuedSet = new WeakSet(); 3968 var revealTimer = null; 3969 3970 function prefersReducedMotion() { 3971 return window.matchMedia && 3972 window.matchMedia("(prefers-reduced-motion: reduce)").matches; 3973 } 3974 3975 function isAuthoringOrStudioContext() { 3976 var docEl = document.documentElement; 3977 var body = document.body; 3978 var host = (window.location.hostname || "").toLowerCase(); 3979 var query = (window.location.search || "").toLowerCase(); 3980 var ref = (document.referrer || "").toLowerCase(); 3981 var snap = ((docEl && docEl.className) || "") + " " + ((body && body.className) || ""); 3982 3983 if (/\b(studio|author|editor|editable|design-mode)\b/.test(snap)) return true; 3984 if (/[?&](studio|authoring|editor|editmode|design)=/.test(query)) return true; 3985 if (host.indexOf("make.powerpages") !== -1) return true; 3986 3987 if (window.self !== window.top) { 3988 return ["powerpages", "make.powerapps", "make.powerpages", "portalstudio"].some(function (signal) { 3989 return ref.indexOf(signal) !== -1; 3990 }); 3991 } 3992 3993 return false; 3994 } 3995 3996 function shouldSkipAutoReveal(node) { 3997 if (!node || !node.matches) return true; 3998 if (node.getAttribute(REVEAL_GROUP_ATTR) === "skip") return true; 3999 return node.matches(SKIP_SELECTOR); 4000 } 4001 4002 function registerAutoRevealGroups() { 4003 [].slice.call(document.querySelectorAll(AUTO_REVEAL_SCOPE)).forEach(function (root) { 4004 if (shouldSkipAutoReveal(root)) return; 4005 4006 if (!root.hasAttribute(REVEAL_GROUP_ATTR)) { 4007 root.setAttribute(REVEAL_GROUP_ATTR, "true"); 4008 } 4009 }); 4010 } 4011 4012 function hydrateRevealGroups() { 4013 var selector = "[" + REVEAL_GROUP_ATTR + "]:not([" + REVEAL_GROUP_ATTR + "='skip'])"; 4014 4015 [].slice.call(document.querySelectorAll(selector)).forEach(function (group) { 4016 if (shouldSkipAutoReveal(group)) return; 4017 if (group.querySelector(".is-reveal, .is-bg-reveal")) return; 4018 4019 [].slice.call(group.children) 4020 .filter(function (child) { 4021 return child && child.nodeType === 1 && !child.matches("script, style, template"); 4022 }) 4023 .forEach(function (child, index) { 4024 if (child.getAttribute(REVEAL_CHILD_INIT_ATTR) === "true") return; 4025 4026 child.setAttribute(REVEAL_CHILD_INIT_ATTR, "true"); 4027 child.classList.add("is-reveal"); 4028 4029 if ( 4030 !child.hasAttribute("data-reveal-delay") && 4031 !child.style.getPropertyValue("--enisa-reveal-delay") 4032 ) { 4033 child.style.setProperty( 4034 "--enisa-reveal-delay", 4035 Math.min(index, STAGGER_MAX) * STAGGER_STEP + "ms" 4036 ); 4037 } 4038 }); 4039 }); 4040 } 4041 4042 function normalizeRevealDelay(el) { 4043 if (!el || !el.getAttribute || !el.style) return; 4044 if (el.style.getPropertyValue("--enisa-reveal-delay")) return; 4045 4046 var raw = (el.getAttribute("data-reveal-delay") || "").trim(); 4047 if (!raw) return; 4048 4049 var parsed = parseInt(raw, 10); 4050 if (isNaN(parsed) || parsed < 0) return; 4051 4052 if (parsed <= 10) { 4053 el.style.setProperty("--enisa-reveal-delay", parsed * STAGGER_STEP + "ms"); 4054 } else { 4055 el.style.setProperty("--enisa-reveal-delay", parsed + "ms"); 4056 } 4057 } 4058 4059 function hasRenderableBox(el) { 4060 if (!el || !el.getClientRects) return false;
4061 if (el.closest && el.closest("[hidden]")) return false; 4062 return el.getClientRects().length > 0; 4063 } 4064 4065 function isInViewport(el) { 4066 if (!hasRenderableBox(el)) return false; 4067 4068 var rect = el.getBoundingClientRect(); 4069 var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; 4070 4071 return rect.bottom > 0 && rect.top < viewportHeight; 4072 } 4073 4074 function sortQueueByViewportOrder() { 4075 revealQueue.sort(function (a, b) { 4076 var ar = a.getBoundingClientRect(); 4077 var br = b.getBoundingClientRect(); 4078 4079 if (Math.abs(ar.top - br.top) > 4) return ar.top - br.top; 4080 return ar.left - br.left; 4081 }); 4082 } 4083 4084 function processRevealQueue() { 4085 if (revealTimer || !revealQueue.length) return; 4086 4087 revealTimer = window.setTimeout(function () { 4088 revealTimer = null; 4089 sortQueueByViewportOrder(); 4090 4091 while (revealQueue.length) { 4092 var el = revealQueue.shift(); 4093 4094 if (!el || el.classList.contains(VISIBLE_CLASS)) continue; 4095 el.classList.add(VISIBLE_CLASS); 4096 } 4097 }, REVEAL_GAP_MS); 4098 } 4099 4100 function enqueueReveal(el) { 4101 if (!el || revealQueuedSet.has(el) || el.classList.contains(VISIBLE_CLASS)) return; 4102 4103 revealQueuedSet.add(el); 4104 revealQueue.push(el); 4105 processRevealQueue(); 4106 } 4107 4108 function getObserver() { 4109 if (observer) return observer; 4110 4111 observer = new IntersectionObserver( 4112 function (entries) { 4113 entries.forEach(function (entry) { 4114 if (!entry.isIntersecting) return; 4115 4116 enqueueReveal(entry.target); 4117 observer.unobserve(entry.target); 4118 }); 4119 }, 4120 { 4121 rootMargin: "0px 0px -12% 0px", 4122 threshold: 0.08 4123 } 4124 ); 4125 4126 return observer; 4127 } 4128 4129 function prepareRevealDom() { 4130 registerAutoRevealGroups(); 4131 hydrateRevealGroups(); 4132 4133 [].slice.call(document.querySelectorAll(REVEAL_SELECTOR)).forEach(function (el) { 4134 normalizeRevealDelay(el); 4135 }); 4136 } 4137 4138 function startReveal() { 4139 if (revealStarted) return; 4140 revealStarted = true; 4141 4142 if (fallbackTimer) { 4143 window.clearTimeout(fallbackTimer); 4144 fallbackTimer = null; 4145 } 4146 4147 prepareRevealDom(); 4148 4149 var elements = [].slice.call(document.querySelectorAll(REVEAL_SELECTOR)); 4150 if (!elements.length) return; 4151 4152 if (prefersReducedMotion() || !("IntersectionObserver" in window)) { 4153 elements.forEach(function (el) { 4154 el.setAttribute(INIT_ATTR, "true"); 4155 el.classList.add(VISIBLE_CLASS); 4156 }); 4157 return; 4158 } 4159 4160 var obs = getObserver(); 4161 4162 elements.forEach(function (el) { 4163 if (el.getAttribute(INIT_ATTR) === "true") return; 4164 4165 el.setAttribute(INIT_ATTR, "true"); 4166 4167 if (isInViewport(el)) { 4168 enqueueReveal(el); 4169 return; 4170 } 4171 4172 obs.observe(el); 4173 }); 4174 } 4175 4176 function initReveal() { 4177 prepareRevealDom(); 4178 4179 if (isAuthoringOrStudioContext() || prefersReducedMotion()) { 4180 startReveal(); 4181 return; 4182 } 4183 4184 if (document.documentElement.getAttribute(HERO_READY_ATTR) === "true") { 4185 startReveal(); 4186 return; 4187 } 4188 4189 document.addEventListener(HERO_READY_EVENT, startReveal, { once: true }); 4190 4191 fallbackTimer = window.setTimeout(function () { 4192 startReveal(); 4193 }, FALLBACK_GATE_MS); 4194 } 4195 4196 if (document.readyState === "loading") { 4197 document.addEventListener("DOMContentLoaded", initReveal); 4198 } else { 4199 initReveal(); 4200 } 4201 4202 if ("MutationObserver" in window) { 4203 var mo = new MutationObserver(function (mutations) { 4204 if (!mutations.some(function (m) { return m.addedNodes.length > 0; })) return; 4205 4206 window.clearTimeout(moTimer); 4207 moTimer = window.setTimeout(function () { 4208 prepareRevealDom(); 4209 4210 if (revealStarted) { 4211 startReveal(); 4212 } 4213 }, 150); 4214 }); 4215 4216 mo.observe(MO_SCOPE, { 4217 childList: true, 4218 subtree: true 4219 }); 4220 } 4221})(); 4222 4223/* 4224 ENISA | JS Module 4225 Modulo: 10-hero-slider 4226 Descripcion: Logica del modulo. 4227*/ 4228(function () { 4229 "use strict"; 4230 4231 var ROOT_SELECTOR = "[data-hero-slider]"; 4232 var SLIDE_SELECTOR = "[data-hero-slide]"; 4233 var DOT_SELECTOR = "[data-hero-dot]"; 4234 var PREV_SELECTOR = "[data-hero-prev]"; 4235 var NEXT_SELECTOR = "[data-hero-next]"; 4236 var STATUS_SELECTOR = "[data-hero-status]"; 4237 var INIT_ATTR = "data-hero-slider-init";
4238 var ACTIVE_CLASS = "is-active"; 4239 var ENTER_CLASS = "is-entering"; 4240 4241 function prefersReducedMotion() { 4242 return window.matchMedia("(prefers-reduced-motion: reduce)").matches; 4243 } 4244 4245 function toBoolean(value) { 4246 if (!value) return false; 4247 var normalized = String(value).toLowerCase().trim(); 4248 return normalized === "true" || normalized === "1" || normalized === "yes"; 4249 } 4250 4251 function getInterval(value) { 4252 var parsed = parseInt(value, 10); 4253 if (isNaN(parsed)) return 7000; 4254 return Math.min(20000, Math.max(3000, parsed)); 4255 } 4256 4257 function setSlideFocusableState(slide, active) { 4258 var focusables = [].slice.call( 4259 slide.querySelectorAll("a[href], button, input, select, textarea, [tabindex]") 4260 ); 4261 4262 focusables.forEach(function (el) { 4263 if (active) { 4264 if (el.hasAttribute("data-hero-tabindex")) { 4265 var stored = el.getAttribute("data-hero-tabindex"); 4266 if (stored === "") { 4267 el.removeAttribute("tabindex"); 4268 } else { 4269 el.setAttribute("tabindex", stored); 4270 } 4271 el.removeAttribute("data-hero-tabindex"); 4272 } 4273 return; 4274 } 4275 4276 if (!el.hasAttribute("data-hero-tabindex")) { 4277 var current = el.getAttribute("tabindex"); 4278 if (current === null) { 4279 el.setAttribute("data-hero-tabindex", ""); 4280 } else { 4281 el.setAttribute("data-hero-tabindex", current); 4282 } 4283 } 4284 4285 el.setAttribute("tabindex", "-1"); 4286 }); 4287 } 4288 4289 function safeFocus(element) { 4290 if ( 4291 window.ENISA && 4292 window.ENISA.utils && 4293 typeof window.ENISA.utils.safeFocus === "function" 4294 ) { 4295 return window.ENISA.utils.safeFocus(element); 4296 } 4297 4298 if (!element || typeof element.focus !== "function") return false; 4299 4300 try { 4301 element.focus({ preventScroll: true }); 4302 } catch (error) { 4303 element.focus(); 4304 } 4305 4306 return true; 4307 } 4308 4309 function initSlider(root) { 4310 if (!root || root.getAttribute(INIT_ATTR) === "true") return; 4311 root.setAttribute(INIT_ATTR, "true"); 4312 4313 var slides = [].slice.call(root.querySelectorAll(SLIDE_SELECTOR)); 4314 var dots = [].slice.call(root.querySelectorAll(DOT_SELECTOR)); 4315 var prevBtn = root.querySelector(PREV_SELECTOR); 4316 var nextBtn = root.querySelector(NEXT_SELECTOR); 4317 var status = root.querySelector(STATUS_SELECTOR); 4318 var total = slides.length; 4319 if (!total) return; 4320 4321 var reducedMotion = prefersReducedMotion(); 4322 var autoplayEnabled = toBoolean(root.getAttribute("data-autoplay")); 4323 var autoplayInterval = getInterval(root.getAttribute("data-autoplay-interval")); 4324 var current = 0; 4325 var autoplayTimer = null; 4326 var isHoverPaused = false; 4327 var isFocusPaused = false; 4328 4329 slides.forEach(function (slide, index) { 4330 if (slide.classList.contains(ACTIVE_CLASS)) { 4331 current = index; 4332 } 4333 }); 4334 4335 function stopAutoplay() { 4336 if (!autoplayTimer) return; 4337 clearTimeout(autoplayTimer); 4338 autoplayTimer = null; 4339 } 4340 4341 function canAutoplay() { 4342 return ( 4343 autoplayEnabled && 4344 total > 1 && 4345 !reducedMotion && 4346 !document.hidden && 4347 !isHoverPaused && 4348 !isFocusPaused 4349 ); 4350 } 4351 4352 function scheduleAutoplay() { 4353 stopAutoplay(); 4354 if (!canAutoplay()) return; 4355 4356 autoplayTimer = setTimeout(function () { 4357 goTo(current + 1); 4358 }, autoplayInterval); 4359 } 4360 4361 function restartEntryAnimation(slide) { 4362 slide.classList.remove(ENTER_CLASS); 4363 if (!reducedMotion) { 4364 void slide.offsetWidth; 4365 } 4366 slide.classList.add(ENTER_CLASS); 4367 } 4368 4369 function updateStatus() { 4370 if (!status) return; 4371 var activeSlide = slides[current]; 4372 var label = activeSlide && activeSlide.getAttribute("aria-label"); 4373 status.textContent = label || ("Diapositiva " + (current + 1) + " de " + total); 4374 } 4375 4376 // animate=false en el primer render: respetamos la clase is-entering que 4377 // ya viene del markup del servidor. Reiniciarla aqui es lo que provoca el 4378 // corte "aparece / desaparece / reaparece" en el hero. 4379 function updateUI(animate) {
4380 slides.forEach(function (slide, index) { 4381 var active = index === current; 4382 slide.classList.toggle(ACTIVE_CLASS, active); 4383 if (!active) { 4384 slide.classList.remove(ENTER_CLASS); 4385 } 4386 slide.setAttribute("aria-hidden", active ? "false" : "true"); 4387 setSlideFocusableState(slide, active); 4388 if (active && animate) { 4389 restartEntryAnimation(slide); 4390 } 4391 }); 4392 4393 dots.forEach(function (dot, index) { 4394 var active = index === current; 4395 dot.classList.toggle(ACTIVE_CLASS, active); 4396 dot.setAttribute("aria-current", active ? "true" : "false"); 4397 dot.setAttribute("aria-selected", active ? "true" : "false"); 4398 dot.setAttribute("tabindex", active ? "0" : "-1"); 4399 }); 4400 4401 updateStatus(); 4402 } 4403 4404 function goTo(index) { 4405 if (total < 2) return; 4406 var nextIndex = (index + total) % total; 4407 if (nextIndex === current) { 4408 scheduleAutoplay(); 4409 return; 4410 } 4411 4412 current = nextIndex; 4413 updateUI(true); 4414 scheduleAutoplay(); 4415 } 4416 4417 if (prevBtn) { 4418 prevBtn.addEventListener("click", function () { 4419 goTo(current - 1); 4420 }); 4421 } 4422 4423 if (nextBtn) { 4424 nextBtn.addEventListener("click", function () { 4425 goTo(current + 1); 4426 }); 4427 } 4428 4429 dots.forEach(function (dot) { 4430 dot.addEventListener("click", function () { 4431 var index = parseInt(dot.getAttribute("data-hero-dot"), 10); 4432 if (!isNaN(index)) { 4433 goTo(index); 4434 } 4435 }); 4436 4437 dot.addEventListener("keydown", function (event) { 4438 var nextIndex = null; 4439 4440 switch (event.key) { 4441 case "ArrowRight": 4442 case "ArrowDown": 4443 nextIndex = (current + 1) % total; 4444 break; 4445 case "ArrowLeft": 4446 case "ArrowUp": 4447 nextIndex = (current - 1 + total) % total; 4448 break; 4449 case "Home": 4450 nextIndex = 0; 4451 break; 4452 case "End": 4453 nextIndex = total - 1; 4454 break; 4455 default: 4456 return; 4457 } 4458 4459 event.preventDefault(); 4460 event.stopPropagation(); 4461 goTo(nextIndex); 4462 if (dots[nextIndex]) { 4463 safeFocus(dots[nextIndex]); 4464 } 4465 }); 4466 }); 4467 4468 if (autoplayEnabled) { 4469 root.addEventListener("mouseenter", function () { 4470 isHoverPaused = true; 4471 stopAutoplay(); 4472 }); 4473 4474 root.addEventListener("mouseleave", function () { 4475 isHoverPaused = false; 4476 scheduleAutoplay(); 4477 }); 4478 4479 root.addEventListener("focusin", function () { 4480 isFocusPaused = true; 4481 stopAutoplay(); 4482 }); 4483 4484 root.addEventListener("focusout", function (event) { 4485 if (root.contains(event.relatedTarget)) return; 4486 isFocusPaused = false; 4487 scheduleAutoplay(); 4488 }); 4489 4490 document.addEventListener("visibilitychange", function () { 4491 if (document.hidden) { 4492 stopAutoplay(); 4493 return; 4494 } 4495 scheduleAutoplay(); 4496 }); 4497 } 4498 4499 updateUI(false); 4500 scheduleAutoplay(); 4501 } 4502 4503 function init() { 4504 var sliders = [].slice.call(document.querySelectorAll(ROOT_SELECTOR)); 4505 if (!sliders.length) return; 4506 sliders.forEach(initSlider); 4507 } 4508 4509 if (document.readyState === "loading") { 4510 document.addEventListener("DOMContentLoaded", init); 4511 } else { 4512 init(); 4513 } 4514})(); 4515 4516/* 4517 ENISA | JS Module 4518 Modulo: 11-cta 4519 Descripcion: Logica del modulo. 4520*/ 4521(function () { 4522 "use strict"; 4523 4524 var CTA_SELECTOR = "[data-cta-sequence]"; 4525 4526 function initCtaSequence() { 4527 var banners = [].slice.call(document.querySelectorAll(CTA_SELECTOR)); 4528 if (!banners.length) return; 4529 4530 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { 4531 banners.forEach(function (banner) { 4532 banner.classList.add("is-animated"); 4533 }); 4534 return; 4535 } 4536 4537 if (!("IntersectionObserver" in window)) { 4538 banners.forEach(function (banner) { 4539 banner.classList.add("is-animated"); 4540 }); 4541 return; 4542 } 4543 4544 var io = new IntersectionObserver( 4545 function (entries) {
4546 entries.forEach(function (entry) { 4547 if (!entry.isIntersecting) return; 4548 entry.target.classList.add("is-animated"); 4549 io.unobserve(entry.target); 4550 }); 4551 }, 4552 { 4553 threshold: 0.12, 4554 } 4555 ); 4556 4557 banners.forEach(function (banner) { 4558 var rect = banner.getBoundingClientRect(); 4559 if (rect.top < window.innerHeight) { 4560 banner.classList.add("is-animated"); 4561 } else { 4562 io.observe(banner); 4563 } 4564 }); 4565 } 4566 4567 if (document.readyState === "loading") { 4568 document.addEventListener("DOMContentLoaded", initCtaSequence); 4569 } else { 4570 initCtaSequence(); 4571 } 4572})(); 4573 4574(function () { 4575 "use strict"; 4576 4577 var EDITORIAL_LISTING_SELECTOR = "[data-editorial-listing], [data-news-listing]"; 4578 var MOBILE_CAROUSEL_BP = window.ENISA.cardsCarousel.MOBILE_BP; 4579 4580 function initEditorialListing(root) { 4581 if (!root) return; 4582 if (root.getAttribute("data-editorial-init") === "true") return; 4583 4584 root.setAttribute("data-editorial-init", "true"); 4585 4586 var isNewsMode = root.hasAttribute("data-news-listing"); 4587 var attrPrefix = isNewsMode ? "news" : "editorial"; 4588 var featuredClass = isNewsMode ? "enisa-news-card--featured" : "enisa-editorial-card--featured"; 4589 var disableFeatured = root.getAttribute("data-disable-featured") === "true"; 4590 4591 var pageSize = parseInt(root.getAttribute("data-page-size"), 10); 4592 if (isNaN(pageSize) || pageSize < 1) pageSize = 7; 4593 4594 var counterSingular = root.getAttribute("data-counter-singular") || "noticia"; 4595 var counterPlural = root.getAttribute("data-counter-plural") || "noticias"; 4596 var initialFilter = root.getAttribute("data-initial-filter") || "all"; 4597 4598 var filterButtons = [].slice.call(root.querySelectorAll("[data-" + attrPrefix + "-filter]")); 4599 var cards = [].slice.call(root.querySelectorAll("[data-" + attrPrefix + "-card]")); 4600 var counter = root.querySelector("[data-" + attrPrefix + "-counter]"); 4601 if (counter && !counter.getAttribute("aria-live")) { 4602 counter.setAttribute("aria-live", "polite"); 4603 counter.setAttribute("aria-atomic", "true"); 4604 } 4605 var pagination = root.querySelector("[data-" + attrPrefix + "-pagination]"); 4606 var grid = root.querySelector("[data-" + attrPrefix + "-grid]"); 4607 4608 if (!cards.length || !grid) return; 4609 4610 var currentFilter = "all"; 4611 var currentPage = 1; 4612 4613 function normalizeFeaturedValue(value) { 4614 return String(value || "").toLowerCase().trim(); 4615 } 4616 4617 function isCardMarkedAsFeatured(card) { 4618 var value = normalizeFeaturedValue(card.getAttribute("data-" + attrPrefix + "-featured")); 4619 4620 return ( 4621 value === "true" || 4622 value === "1" || 4623 value === "yes" || 4624 value === "si" || 4625 value === "sÃ" 4626 ); 4627 } 4628 4629 function hideEmptySecondaryBadges() { 4630 cards.forEach(function (card) { 4631 var secondaryBadges = [].slice.call( 4632 card.querySelectorAll(".enisa-editorial-card__badge--secondary, .enisa-news-badge--tag") 4633 ); 4634 4635 secondaryBadges.forEach(function (badge) { 4636 if (!badge || !badge.textContent || !badge.textContent.trim()) { 4637 badge.setAttribute("hidden", ""); 4638 } 4639 }); 4640 }); 4641 } 4642 4643 function getFilteredCards() { 4644 if (currentFilter === "all") return cards.slice(); 4645 4646 return cards.filter(function (card) { 4647 return (card.getAttribute("data-category") || "") === currentFilter; 4648 }); 4649 } 4650 4651 function getFeaturedCardForPage(pageCards) { 4652 if (disableFeatured || !pageCards.length) { 4653 return null; 4654 } 4655 4656 var markedFeatured = pageCards.find(isCardMarkedAsFeatured); 4657 4658 if (markedFeatured) { 4659 return markedFeatured; 4660 } 4661 4662 return pageCards[0]; 4663 } 4664 4665 function getOrderedPageCards(pageCards, featuredCard) { 4666 if (!featuredCard) { 4667 return pageCards.slice(); 4668 } 4669 4670 var ordered = pageCards.filter(function (card) { 4671 return card !== featuredCard; 4672 }); 4673 4674 var featuredPosition = Math.min(3, ordered.length); 4675 4676 ordered.splice(featuredPosition, 0, featuredCard); 4677 4678 return ordered; 4679 } 4680 4681 function isMobileCarouselViewport() { 4682 return window.innerWidth <= MOBILE_CAROUSEL_BP; 4683 } 4684 4685 function scrollFilterToStart(activeButton) { 4686 if (!activeButton || !isMobileCarouselViewport()) return; 4687 4688 var filterContainer = activeButton.parentElement; 4689 if (!filterContainer) return; 4690 4691 var nextLeft = Math.max(0, activeButton.offsetLeft - filterContainer.offsetLeft); 4692 4693 filterContainer.scrollTo({ 4694 left: nextLeft, 4695 behavior: window.ENISA.utils.prefersReducedMotion() ? "auto" : "smooth" 4696 }); 4697 } 4698 4699 function safeFocus(element) { 4700 if ( 4701 window.ENISA && 4702 window.ENISA.utils && 4703 typeof window.ENISA.utils.safeFocus === "function" 4704 ) { 4705 return window.ENISA.utils.safeFocus(element); 4706 } 4707 4708 if (!element || typeof element.focus !== "function") return false;
4709 4710 try { 4711 element.focus({ preventScroll: true }); 4712 } catch (error) { 4713 element.focus(); 4714 } 4715 4716 return true; 4717 } 4718 4719 function setFilterState(filter, shouldScrollActive) { 4720 currentFilter = filter || "all"; 4721 currentPage = 1; 4722 4723 var activeButton = null; 4724 4725 filterButtons.forEach(function (btn) { 4726 var buttonFilter = btn.getAttribute("data-" + attrPrefix + "-filter") || ""; 4727 var isActive = buttonFilter === currentFilter; 4728 4729 btn.classList.toggle("is-active", isActive); 4730 btn.setAttribute("aria-pressed", isActive ? "true" : "false"); 4731 4732 if (isActive) { 4733 activeButton = btn; 4734 } 4735 }); 4736 4737 if (!activeButton && currentFilter !== "all") { 4738 currentFilter = "all"; 4739 4740 filterButtons.forEach(function (btn) { 4741 var buttonFilter = btn.getAttribute("data-" + attrPrefix + "-filter") || ""; 4742 var isActive = buttonFilter === "all"; 4743 4744 btn.classList.toggle("is-active", isActive); 4745 btn.setAttribute("aria-pressed", isActive ? "true" : "false"); 4746 4747 if (isActive) { 4748 activeButton = btn; 4749 } 4750 }); 4751 } 4752 4753 if (shouldScrollActive) { 4754 scrollFilterToStart(activeButton); 4755 } 4756 } 4757 4758 function updateCounter(total, from, to) { 4759 if (!counter) return; 4760 4761 var label = total === 1 ? counterSingular : counterPlural; 4762 4763 if (total <= 0) { 4764 counter.textContent = "Mostrado 0 " + counterPlural; 4765 return; 4766 } 4767 4768 counter.textContent = "Mostrado " + from + "-" + to + " de " + total + " " + label; 4769 } 4770 4771 function renderPagination(totalPages) { 4772 if (!pagination) return; 4773 4774 pagination.innerHTML = ""; 4775 4776 if (totalPages <= 1) { 4777 pagination.setAttribute("hidden", ""); 4778 return; 4779 } 4780 4781 pagination.removeAttribute("hidden"); 4782 4783 var WINDOW = 3; 4784 var half = Math.floor(WINDOW / 2); 4785 var start = Math.max(1, currentPage - half); 4786 var end = Math.min(totalPages, start + WINDOW - 1); 4787 4788 if (end - start < WINDOW - 1) { 4789 start = Math.max(1, end - WINDOW + 1); 4790 } 4791 4792 var prevBtn = document.createElement("button"); 4793 prevBtn.type = "button"; 4794 prevBtn.className = "enisa-pagination__btn enisa-pagination__btn--arrow is-prev"; 4795 prevBtn.setAttribute("aria-label", "Página anterior"); 4796 prevBtn.innerHTML = '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 4797 prevBtn.disabled = currentPage === 1; 4798 4799 prevBtn.addEventListener("click", function () { 4800 if (currentPage > 1) { 4801 currentPage -= 1; 4802 render({ type: "prev" }); 4803 } 4804 }); 4805 4806 pagination.appendChild(prevBtn); 4807 4808 var pageList = document.createElement("div"); 4809 pageList.className = "enisa-pagination__list"; 4810 pageList.setAttribute("role", "list"); 4811 4812 for (var i = start; i <= end; i += 1) { 4813 var pageBtn = document.createElement("button"); 4814 pageBtn.type = "button"; 4815 pageBtn.className = "enisa-pagination__btn" + (i === currentPage ? " is-active" : ""); 4816 pageBtn.textContent = String(i); 4817 pageBtn.setAttribute("aria-label", "Página " + i); 4818 if (i === currentPage) { 4819 pageBtn.setAttribute("aria-current", "page"); 4820 } 4821 4822 (function (pageNumber) { 4823 pageBtn.addEventListener("click", function () { 4824 currentPage = pageNumber; 4825 render({ type: "page", page: pageNumber }); 4826 }); 4827 })(i); 4828 4829 pageList.appendChild(pageBtn); 4830 } 4831 4832 pagination.appendChild(pageList); 4833 4834 var nextBtn = document.createElement("button"); 4835 nextBtn.type = "button"; 4836 nextBtn.className = "enisa-pagination__btn enisa-pagination__btn--arrow is-next"; 4837 nextBtn.setAttribute("aria-label", "Página siguiente"); 4838 nextBtn.innerHTML = '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 4839 nextBtn.disabled = currentPage === totalPages; 4840 4841 nextBtn.addEventListener("click", function () { 4842 if (currentPage < totalPages) { 4843 currentPage += 1; 4844 render({ type: "next" }); 4845 } 4846 }); 4847 4848 pagination.appendChild(nextBtn); 4849 } 4850 4851 function getCardRows(pageCards) { 4852 var rows = []; 4853 var current = []; 4854 4855 pageCards.forEach(function (card) { 4856 if (card.classList.contains(featuredClass)) { 4857 if (current.length) { 4858 rows.push(current.slice()); 4859 current = []; 4860 } 4861 4862 rows.push([card]); 4863 return; 4864 } 4865 4866 current.push(card); 4867 4868 if (current.length === 3) { 4869 rows.push(current.slice()); 4870 current = []; 4871 } 4872 }); 4873 4874 if (current.length) { 4875 rows.push(current.slice()); 4876 } 4877 4878 return rows; 4879 } 4880 4881 function createRowCarouselControls(viewport, rowCards, rowIndex) { 4882 var controls = document.createElement("div"); 4883 controls.className = "enisa-editorial-row-controls"; 4884 controls.setAttribute("role", "group"); 4885 controls.setAttribute("aria-label", "Controles de carrusel fila " + (rowIndex + 1)); 4886 4887 var prevBtn = document.createElement("button"); 4888 prevBtn.type = "button"; 4889 prevBtn.className = "enisa-editorial-row-controls__btn is-prev"; 4890 prevBtn.setAttribute("aria-label", "Tarjeta anterior"); 4891 prevBtn.innerHTML = '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 4892 4893 var dotsEl = document.createElement("div"); 4894 dotsEl.className = "enisa-editorial-row-controls__dots"; 4895 dotsEl.setAttribute("role", "group"); 4896 dotsEl.setAttribute("aria-label", "Páginas del carrusel"); 4897 4898 var nextBtn = document.createElement("button"); 4899 nextBtn.type = "button"; 4900 nextBtn.className = "enisa-editorial-row-controls__btn is-next"; 4901 nextBtn.setAttribute("aria-label", "Tarjeta siguiente"); 4902 nextBtn.innerHTML = '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 4903 4904 var dotButtons = rowCards.map(function (card, idx) { 4905 var dot = document.createElement("button"); 4906 dot.type = "button"; 4907 dot.className = "enisa-editorial-row-controls__dot"; 4908 dot.setAttribute("aria-label", "Ir a tarjeta " + (idx + 1)); 4909 dot.setAttribute("tabindex", idx === 0 ? "0" : "-1"); 4910 4911 if (idx === 0) { 4912 dot.classList.add("is-active"); 4913 } 4914 4915 dotsEl.appendChild(dot); 4916 return dot; 4917 }); 4918 4919 controls.appendChild(prevBtn); 4920 controls.appendChild(dotsEl); 4921 controls.appendChild(nextBtn); 4922 4923 window.ENISA.utils.createScrollCarousel({ 4924 viewport: viewport, 4925 items: rowCards, 4926 prevBtn: prevBtn, 4927 nextBtn: nextBtn, 4928 dots: dotButtons, 4929 activeDotClass: "is-active" 4930 }); 4931 4932 return controls; 4933 } 4934 4935 function renderEditorialRows(pageCards) { 4936 grid.innerHTML = ""; 4937 4938 var rows = getCardRows(pageCards); 4939 4940 rows.forEach(function (rowCards, rowIndex) { 4941 var row = document.createElement("div"); 4942 var isCarousel = rowCards.length > 1; 4943 4944 row.className = "enisa-editorial-grid__row " + (isCarousel ? "is-carousel" : "is-single"); 4945 4946 if (isCarousel) { 4947 var viewport = document.createElement("div"); 4948 viewport.className = "enisa-editorial-grid__viewport"; 4949 viewport.setAttribute("data-editorial-row-viewport", ""); 4950 4951 var track = document.createElement("div"); 4952 track.className = "enisa-editorial-grid__track"; 4953 4954 rowCards.forEach(function (card) { 4955 card.hidden = false;
4956 card.style.minHeight = ""; 4957 track.appendChild(card); 4958 }); 4959 4960 viewport.appendChild(track); 4961 row.appendChild(viewport); 4962 row.appendChild(createRowCarouselControls(viewport, rowCards, rowIndex)); 4963 } else { 4964 rowCards.forEach(function (card) { 4965 card.hidden = false; 4966 card.style.minHeight = ""; 4967 row.appendChild(card); 4968 }); 4969 } 4970 4971 grid.appendChild(row); 4972 }); 4973 } 4974 4975 function syncMobileRowHeights() { 4976 var isMobile = window.innerWidth <= MOBILE_CAROUSEL_BP; 4977 var carouselRows = [].slice.call(grid.querySelectorAll(".enisa-editorial-grid__row.is-carousel")); 4978 4979 carouselRows.forEach(function (row) { 4980 var rowCards = [].slice.call(row.querySelectorAll(".enisa-editorial-card, .enisa-news-card")); 4981 4982 rowCards.forEach(function (card) { 4983 card.style.minHeight = ""; 4984 }); 4985 4986 if (!isMobile || rowCards.length < 2) return; 4987 4988 window.ENISA.cardsCarousel.syncEqualHeights(rowCards); 4989 }); 4990 } 4991 4992 function getPaginationFocusTarget(focusTarget) { 4993 if (!focusTarget || !pagination) return null; 4994 4995 if (focusTarget.type === "prev") { 4996 return pagination.querySelector(".enisa-pagination__btn--arrow.is-prev"); 4997 } 4998 4999 if (focusTarget.type === "next") { 5000 return pagination.querySelector(".enisa-pagination__btn--arrow.is-next"); 5001 } 5002 5003 if (focusTarget.type === "page") { 5004 return [].slice.call(pagination.querySelectorAll(".enisa-pagination__list .enisa-pagination__btn")).find(function (btn) { 5005 return btn.textContent === String(focusTarget.page); 5006 }) || null; 5007 } 5008 5009 return null; 5010 } 5011 5012 function render(focusTarget) { 5013 var filtered = getFilteredCards(); 5014 var total = filtered.length; 5015 var totalPages = Math.max(1, Math.ceil(total / pageSize)); 5016 var activeCard = document.activeElement && document.activeElement.closest 5017 ? document.activeElement.closest("[data-" + attrPrefix + "-card]") 5018 : null; 5019 5020 if (currentPage > totalPages) currentPage = totalPages; 5021 if (currentPage < 1) currentPage = 1; 5022 5023 var start = (currentPage - 1) * pageSize; 5024 var end = start + pageSize; 5025 var pageCards = filtered.slice(start, end); 5026 var featuredCard = getFeaturedCardForPage(pageCards); 5027 var orderedPageCards = getOrderedPageCards(pageCards, featuredCard); 5028 5029 if (activeCard && orderedPageCards.indexOf(activeCard) === -1) { 5030 var fallbackFocus = getPaginationFocusTarget(focusTarget) || 5031 filterButtons.filter(function (button) { 5032 return button.getAttribute("aria-pressed") === "true"; 5033 })[0] || 5034 filterButtons[0]; 5035 5036 if ( 5037 fallbackFocus && 5038 window.ENISA && 5039 window.ENISA.utils && 5040 typeof window.ENISA.utils.focusWithoutScroll === "function" 5041 ) { 5042 window.ENISA.utils.focusWithoutScroll(fallbackFocus); 5043 } else if (fallbackFocus && typeof fallbackFocus.focus === "function") { 5044 safeFocus(fallbackFocus); 5045 } 5046 } 5047 5048 cards.forEach(function (card) { 5049 card.hidden = true; 5050 card.classList.remove(featuredClass); 5051 }); 5052 5053 if (featuredCard) { 5054 featuredCard.classList.add(featuredClass); 5055 } 5056 5057 renderEditorialRows(orderedPageCards); 5058 5059 var from = total ? start + 1 : 0; 5060 var to = total ? start + pageCards.length : 0; 5061 5062 updateCounter(total, from, to); 5063 renderPagination(totalPages); 5064 5065 window.requestAnimationFrame(function () { 5066 syncMobileRowHeights(); 5067 5068 var nextFocusTarget = getPaginationFocusTarget(focusTarget); 5069 5070 if (nextFocusTarget && typeof nextFocusTarget.focus === "function" && document.contains(nextFocusTarget)) { 5071 safeFocus(nextFocusTarget); 5072 } 5073 }); 5074 } 5075 5076 filterButtons.forEach(function (btn) { 5077 btn.addEventListener("click", function () { 5078 var nextFilter = btn.getAttribute("data-" + attrPrefix + "-filter") || "all"; 5079 5080 setFilterState(nextFilter, true); 5081 render(null); 5082 }); 5083 }); 5084 5085 hideEmptySecondaryBadges(); 5086 setFilterState(initialFilter, false); 5087 render(null); 5088 5089 window.addEventListener("resize", window.ENISA.utils.debounce(function () { 5090 syncMobileRowHeights(); 5091 }, 150)); 5092 } 5093
5094 function init() { 5095 var listings = document.querySelectorAll(EDITORIAL_LISTING_SELECTOR); 5096 5097 if (!listings.length) return; 5098 5099 [].forEach.call(listings, initEditorialListing); 5100 } 5101 5102 if (document.readyState === "loading") { 5103 document.addEventListener("DOMContentLoaded", init); 5104 } else { 5105 init(); 5106 } 5107})(); 5108 5109/* 5110 ENISA | JS Module 5111 Modulo: 13-carousels-latest 5112 Descripcion: Logica del modulo. 5113*/ 5114(function () { 5115 "use strict"; 5116 5117 var DEFAULT_DESKTOP_MIN_CARD_W = 328; 5118 var DEFAULT_MOBILE_MIN_CARD_W = 268; 5119 var DEFAULT_CAROUSEL_GAP = 24; 5120 5121 function initSimpleLatestCarousel(root, config) { 5122 if (!root) return; 5123 if (root.getAttribute(config.initAttr) === "true") return; 5124 root.setAttribute(config.initAttr, "true"); 5125 5126 var viewport = root.querySelector(config.viewportSelector); 5127 if (!viewport) return; 5128 5129 var cards = [].slice.call(viewport.querySelectorAll(config.cardSelector)); 5130 if (cards.length < 2) return; 5131 5132 var controls = document.createElement("div"); 5133 controls.className = "enisa-editorial-row-controls"; 5134 controls.setAttribute("role", "group"); 5135 controls.setAttribute("aria-label", config.controlsLabel); 5136 5137 var prevBtn = document.createElement("button"); 5138 prevBtn.type = "button"; 5139 prevBtn.className = "enisa-editorial-row-controls__btn is-prev"; 5140 prevBtn.setAttribute("aria-label", "Elemento anterior. " + config.controlsLabel); 5141 prevBtn.innerHTML = '<span class="enisa-icon" aria-hidden="true"></span>'; 5142 5143 var dotsEl = document.createElement("div"); 5144 dotsEl.className = "enisa-editorial-row-controls__dots"; 5145 dotsEl.setAttribute("role", "group"); 5146 dotsEl.setAttribute("aria-label", "Páginas del carrusel"); 5147 5148 var nextBtn = document.createElement("button"); 5149 nextBtn.type = "button"; 5150 nextBtn.className = "enisa-editorial-row-controls__btn is-next"; 5151 nextBtn.setAttribute("aria-label", "Elemento siguiente. " + config.controlsLabel); 5152 nextBtn.innerHTML = '<span class="enisa-icon" aria-hidden="true"></span>'; 5153 5154 controls.appendChild(prevBtn); 5155 controls.appendChild(dotsEl); 5156 controls.appendChild(nextBtn); 5157 viewport.insertAdjacentElement("afterend", controls); 5158 controls.setAttribute("hidden", ""); 5159 5160 var carousel = window.ENISA.utils.createScrollCarousel({ 5161 viewport: viewport, 5162 items: cards, 5163 prevBtn: prevBtn, 5164 nextBtn: nextBtn, 5165 dots: [], 5166 activeDotClass: "is-active" 5167 }); 5168 5169 var mobileBreakpoint = typeof config.mobileBreakpoint === "number" 5170 ? config.mobileBreakpoint 5171 : window.ENISA.cardsCarousel.MOBILE_BP; 5172 5173 var lastPerPage = 0; 5174 var shouldSyncEqualHeights = typeof config.syncEqualHeights === "boolean" 5175 ? config.syncEqualHeights 5176 : true; 5177 5178 function getAvailableWidth() { 5179 var container = viewport.parentElement || root; 5180 var styles = window.getComputedStyle ? window.getComputedStyle(container) : null; 5181 var padding = styles 5182 ? parseFloat(styles.paddingLeft || "0") + parseFloat(styles.paddingRight || "0") 5183 : 0; 5184 return Math.floor((container.getBoundingClientRect().width || container.clientWidth) - padding); 5185 } 5186 5187 function shouldUseCarousel() { 5188 if (config.forceBelowBreakpoint) { 5189 var isCoarsePointer = window.matchMedia 5190 ? window.matchMedia("(pointer: coarse)").matches 5191 : false; 5192 5193 if (window.innerWidth > mobileBreakpoint && !isCoarsePointer) { 5194 return false; 5195 } 5196 } 5197 5198 var styles = window.getComputedStyle ? window.getComputedStyle(viewport) : null; 5199 var desktopMinCardW = styles 5200 ? parseFloat(styles.getPropertyValue("--enisa-carousel-min-card-w") || "") 5201 : NaN; 5202 var mobileMinCardW = styles 5203 ? parseFloat(styles.getPropertyValue("--enisa-carousel-min-card-w-mobile") || "") 5204 : NaN; 5205 var minCardW = window.innerWidth <= mobileBreakpoint 5206 ? (isNaN(mobileMinCardW) || mobileMinCardW <= 0 5207 ? (isNaN(desktopMinCardW) || desktopMinCardW <= 0 5208 ? DEFAULT_MOBILE_MIN_CARD_W 5209 : desktopMinCardW) 5210 : mobileMinCardW) 5211 : (isNaN(desktopMinCardW) || desktopMinCardW <= 0 5212 ? DEFAULT_DESKTOP_MIN_CARD_W 5213 : desktopMinCardW); 5214 var gap = styles 5215 ? parseFloat(styles.getPropertyValue("--enisa-carousel-gap") || "") 5216 : NaN; 5217 if (isNaN(gap) || gap <= 0) gap = DEFAULT_CAROUSEL_GAP; 5218 var n = cards.length; 5219 var availableW = getAvailableWidth(); 5220 var required = n * minCardW + (n - 1) * gap; 5221 return availableW > 0 && required > availableW + 1; 5222 } 5223 5224 function calcPerPage() { 5225 if (!cards.length) return 1; 5226 var styles = window.getComputedStyle ? window.getComputedStyle(viewport) : null; 5227 var gap = styles ? parseFloat(styles.getPropertyValue("--enisa-carousel-gap") || "24") : 24; 5228 var cardW = cards[0].getBoundingClientRect().width; 5229 if (!cardW) return 1; 5230 var containerW = viewport.getBoundingClientRect().width || viewport.clientWidth; 5231 return Math.max(1, Math.floor((containerW + gap) / (cardW + ga
5231p))); 5232 } 5233 5234 function buildPageDots(perPage) { 5235 var n = cards.length; 5236 var numPages = Math.ceil(n / perPage); 5237 dotsEl.innerHTML = ""; 5238 var newDots = []; 5239 for (var p = 0; p < numPages; p++) { 5240 var dot = document.createElement("button"); 5241 dot.type = "button"; 5242 dot.className = "enisa-editorial-row-controls__dot"; 5243 dot.setAttribute("aria-label", "Ir al elemento " + (p + 1) + " de " + numPages); 5244 dot.setAttribute("aria-current", p === 0 ? "true" : "false"); 5245 if (p === 0) dot.classList.add("is-active"); 5246 dotsEl.appendChild(dot); 5247 newDots.push(dot); 5248 } 5249 return newDots; 5250 } 5251 5252 function syncCardHeights() { 5253 cards.forEach(function (card) { card.style.minHeight = ""; }); 5254 if (root.classList.contains("is-carousel") && shouldSyncEqualHeights) { 5255 window.ENISA.cardsCarousel.syncEqualHeights(cards); 5256 } 5257 } 5258 5259 function setTrailingPadding() { 5260 viewport.style.paddingInlineEnd = ""; 5261 } 5262 5263 function setControlsVisibility() { 5264 var useCarousel = shouldUseCarousel(); 5265 root.classList.toggle("is-carousel", useCarousel); 5266 if (useCarousel) { 5267 var perPage = calcPerPage(); 5268 if (perPage !== lastPerPage) { 5269 lastPerPage = perPage; 5270 var newDots = buildPageDots(perPage); 5271 carousel.setDots(newDots, "is-active"); 5272 carousel.setPerPage(perPage); 5273 } 5274 controls.removeAttribute("hidden"); 5275 [].slice.call(controls.querySelectorAll("button")).forEach(function (b) { b.removeAttribute("tabindex"); }); 5276 syncCardHeights(); 5277 setTrailingPadding(); 5278 carousel.sync(); 5279 } else { 5280 cards.forEach(function (card) { card.style.minHeight = ""; }); 5281 lastPerPage = 0; 5282 viewport.style.paddingInlineEnd = ""; 5283 carousel.reset(); 5284 [].slice.call(controls.querySelectorAll("button")).forEach(function (b) { b.setAttribute("tabindex", "-1"); }); 5285 controls.setAttribute("hidden", ""); 5286 } 5287 } 5288 5289 setControlsVisibility(); 5290 window.addEventListener("load", setControlsVisibility, { once: true }); 5291 window.addEventListener("resize", window.ENISA.utils.debounce(setControlsVisibility, 150)); 5292 } 5293 5294 function initStudiesLatest(root) { 5295 initSimpleLatestCarousel(root, { 5296 initAttr: "data-studies-latest-init", 5297 viewportSelector: "[data-studies-latest-carousel]", 5298 cardSelector: "[data-studies-latest-card], .enisa-studies-card", 5299 controlsLabel: "Controles del carrusel de estudios", 5300 mobileBreakpoint: window.ENISA.cardsCarousel.MOBILE_BP 5301 }); 5302 } 5303 5304 function initNewsLatest(root) { 5305 var isBulletinsLatest = root.classList.contains("enisa-bulletins-latest"); 5306 var isGalleryLatest = root.classList.contains("enisa-news-latest--gallery"); 5307 initSimpleLatestCarousel(root, { 5308 initAttr: "data-news-latest-init", 5309 viewportSelector: "[data-news-latest-carousel]", 5310 cardSelector: isGalleryLatest 5311 ? "[data-news-latest-card], .enisa-press-media__card" 5312 : "[data-news-latest-card], .enisa-news-card", 5313 controlsLabel: isGalleryLatest 5314 ? "Controles del carrusel de galeria" 5315 : "Controles del carrusel de ultimas noticias", 5316 mobileBreakpoint: isGalleryLatest ? 991.98 : window.ENISA.cardsCarousel.MOBILE_BP, 5317 syncEqualHeights: !isBulletinsLatest && !isGalleryLatest, 5318 forceBelowBreakpoint: isGalleryLatest 5319 }); 5320 } 5321 5322 function initCardsRecursos(root) { 5323 initSimpleLatestCarousel(root, { 5324 initAttr: "data-cards-recursos-init", 5325 viewportSelector: ".enisa-cards-recursos__grid, .enisa-cards-recursos__grid--2, .enisa-cards-recursos__grid--3, .enisa-cards-recursos__grid--4", 5326 cardSelector: ".enisa-cards-recursos__card", 5327 controlsLabel: "Controles del carrusel de recursos", 5328 mobileBreakpoint: 1199.98 5329 }); 5330 } 5331 5332 function initBlogLatest(root) { 5333 initSimpleLatestCarousel(root, { 5334 initAttr: "data-blog-latest-init", 5335 viewportSelector: ".enisa-editorial-latest__grid", 5336 cardSelector: "[data-editorial-latest-card], .enisa-editorial-latest-card", 5337 controlsLabel: "Controles del carrusel de blog", 5338 mobileBreakpoint: 1023 5339 }); 5340 } 5341 5342 function initPodcastLatest(root) { 5343 initSimpleLatestCarousel(root, { 5344 initAttr: "data-podcast-latest-init", 5345 viewportSelector: ".enisa-podcast-programas__list", 5346 cardSelector: ".enisa-podcast-programa-card", 5347 controlsLabel: "Controles del carrusel de podcast", 5348 mobileBreakpoint: window.ENISA.cardsCarousel.MOBILE_BP 5349 }); 5350 } 5351 5352 function initNewsRelated(root) { 5353 initSimpleLatestCarousel(root, { 5354 initAttr: "data-news-related-init", 5355 viewportSelector: "[data-news-related-grid], .enisa-news-related__grid", 5356 cardSelector: "[data-news-related-card], .enisa-news-card", 5357 controlsLabel: "Controles del carrusel de noticias relacionadas", 5358 mobileBreakpoint: window.ENISA.cardsCarousel.MOBILE_BP 5359 }); 5360 } 5361
5362 function init() { 5363 var studiesSections = document.querySelectorAll("[data-studies-latest]"); 5364 [].forEach.call(studiesSections, initStudiesLatest); 5365 5366 var newsSections = document.querySelectorAll("[data-news-latest]"); 5367 [].forEach.call(newsSections, initNewsLatest); 5368 5369 var recursosSections = document.querySelectorAll("[data-cmp-cards-recursos]"); 5370 [].forEach.call(recursosSections, initCardsRecursos); 5371 5372 var blogSections = document.querySelectorAll(".enisa-editorial-latest"); 5373 [].forEach.call(blogSections, initBlogLatest); 5374 5375 var podcastSections = document.querySelectorAll("[data-podcast-latest]"); 5376 [].forEach.call(podcastSections, initPodcastLatest); 5377 5378 var relatedSections = document.querySelectorAll("[data-news-related]"); 5379 [].forEach.call(relatedSections, initNewsRelated); 5380 } 5381 5382 if (document.readyState === "loading") { 5383 document.addEventListener("DOMContentLoaded", init); 5384 } else { 5385 init(); 5386 } 5387})(); 5388 5389/* 5390 ENISA | JS Module 5391 Modulo: 15-carousels-fin 5392 Descripcion: Logica del modulo. 5393*/ 5394(function () { 5395 "use strict"; 5396 5397 var SECTION_SELECTOR = "[data-fin-sectores], [data-fin-detalles], .cmp-hero-metricas"; 5398 var TRACK_SELECTOR = "[data-fin-track]"; 5399 var DOT_SELECTOR = "[data-fin-dot]"; 5400 var PREV_SELECTOR = "[data-fin-prev]"; 5401 var NEXT_SELECTOR = "[data-fin-next]"; 5402 var METRICAS_TRACK_SELECTOR = ".cmp-hero-metricas__track"; 5403 var METRICAS_CARD_WIDTH = 295.5; 5404 var METRICAS_GAP = 8; 5405 var CAROUSEL_BP = window.ENISA.cardsCarousel.MOBILE_BP; 5406 var DEFAULT_ACTIVE_DOT_CLASS = "is-active"; 5407 var INIT_ATTR = "data-fin-carousel-init"; 5408 var WATCH_ATTR = "data-fin-carousel-watch"; 5409 5410 function initCarousel(section) { 5411 if (!section || section.getAttribute(INIT_ATTR) === "true") return true; 5412 5413 var isMetricas = section.classList.contains("cmp-hero-metricas"); 5414 var track = section.querySelector(TRACK_SELECTOR) || section.querySelector(METRICAS_TRACK_SELECTOR); 5415 var dots = [].slice.call(section.querySelectorAll(DOT_SELECTOR)); 5416 var prevBtn = section.querySelector(PREV_SELECTOR); 5417 var nextBtn = section.querySelector(NEXT_SELECTOR); 5418 var items = [].slice.call(track ? track.children : []); 5419 5420 if (!track || !items.length) return false; 5421 5422 section.setAttribute(INIT_ATTR, "true"); 5423 5424 var activeDotClass = isMetricas ? "is-active" : DEFAULT_ACTIVE_DOT_CLASS; 5425 var customCarouselBp = parseFloat(section.getAttribute("data-fin-carousel-bp") || ""); 5426 var effectiveCarouselBp = !isNaN(customCarouselBp) && customCarouselBp > 0 5427 ? customCarouselBp 5428 : CAROUSEL_BP; 5429 if (!isMetricas && dots.length) { 5430 var detected = [].slice.call(dots[0].classList).find(function (cls) { 5431 return /--active$/.test(cls); 5432 }); 5433 if (detected) activeDotClass = detected; 5434 } 5435 5436 var carousel = window.ENISA.utils.createScrollCarousel({ 5437 viewport: track, 5438 items: items, 5439 prevBtn: prevBtn, 5440 nextBtn: nextBtn, 5441 dots: dots, 5442 activeDotClass: activeDotClass 5443 }); 5444 5445 var lastSectorsPerPage = 0; 5446 5447 function shouldUseMetricasCarousel() { 5448 return window.innerWidth <= effectiveCarouselBp; 5449 } 5450 5451 function shouldUseSectorsCarousel() { 5452 var styles = window.getComputedStyle ? window.getComputedStyle(track) : null; 5453 var minCardW = styles 5454 ? parseFloat(styles.getPropertyValue("--enisa-carousel-min-card-w") || "0") 5455 : 0; 5456 if (!minCardW || minCardW <= 0) return window.innerWidth <= effectiveCarouselBp; 5457 var gap = styles ? parseFloat(styles.getPropertyValue("--enisa-carousel-gap") || "24") : 24; 5458 var container = track.parentElement || section; 5459 var cStyles = window.getComputedStyle ? window.getComputedStyle(container) : null; 5460 var padding = cStyles 5461 ? parseFloat(cStyles.paddingLeft || "0") + parseFloat(cStyles.paddingRight || "0") 5462 : 0; 5463 var availableW = Math.floor((container.getBoundingClientRect().width || container.clientWidth) - padding); 5464 var required = items.length * minCardW + (items.length - 1) * gap; 5465 return availableW > 0 && required > availableW + 1; 5466 } 5467 5468 function calcSectorsPerPage() { 5469 if (!items.length) return 1; 5470 var styles = window.getComputedStyle ? window.getComputedStyle(track) : null; 5471 var gap = styles ? parseFloat(styles.getPropertyValue("--enisa-carousel-gap") || "24") : 24; 5472 var cardW = items[0].getBoundingClientRect().width; 5473 if (!cardW) return 1; 5474 var containerW = track.getBoundingClientRect().width || track.clientWidth; 5475 return Math.max(1, Math.floor((containerW + gap) / (cardW + ga
5475p))); 5476 } 5477 5478 function syncSectorsDots(perPage) { 5479 var numPages = Math.ceil(items.length / perPage); 5480 dots.forEach(function (dot, i) { 5481 dot.hidden = i >= numPages; 5482 }); 5483 carousel.setPerPage(perPage); 5484 } 5485 5486 function isCarouselViewport() { 5487 if (isMetricas) return shouldUseMetricasCarousel(); 5488 return shouldUseSectorsCarousel(); 5489 } 5490 5491 function syncCardHeights() { 5492 items.forEach(function (item) { 5493 item.style.minHeight = ""; 5494 }); 5495 5496 if (isMetricas) return; 5497 if (!isCarouselViewport()) return; 5498 window.ENISA.cardsCarousel.syncEqualHeights(items); 5499 } 5500 5501 function setTrailingPadding() { 5502 if (isMetricas || !items.length) return; 5503 track.style.paddingInlineEnd = ""; 5504 } 5505 5506 function syncCarouselMode() { 5507 var useCarousel = isCarouselViewport(); 5508 section.classList.toggle("is-carousel", useCarousel); 5509 5510 var htmlControls = section.querySelector("[data-fin-controls]"); 5511 if (htmlControls) { 5512 var controlBtns = [].slice.call(htmlControls.querySelectorAll("button")); 5513 if (useCarousel) { 5514 htmlControls.removeAttribute("hidden"); 5515 htmlControls.removeAttribute("aria-hidden"); 5516 controlBtns.forEach(function (b) { b.removeAttribute("tabindex"); }); 5517 } else { 5518 htmlControls.setAttribute("hidden", ""); 5519 htmlControls.setAttribute("aria-hidden", "true"); 5520 controlBtns.forEach(function (b) { b.setAttribute("tabindex", "-1"); }); 5521 } 5522 } 5523 5524 if (!isMetricas && useCarousel) { 5525 var perPage = calcSectorsPerPage(); 5526 if (perPage !== lastSectorsPerPage) { 5527 lastSectorsPerPage = perPage; 5528 syncSectorsDots(perPage); 5529 } 5530 setTrailingPadding(true); 5531 } else if (!isMetricas) { 5532 setTrailingPadding(false); 5533 } 5534 } 5535 5536 syncCarouselMode(); 5537 syncCardHeights(); 5538 if (isCarouselViewport()) carousel.sync(); 5539 5540 window.addEventListener("load", function () { 5541 syncCarouselMode(); 5542 syncCardHeights(); 5543 if (isCarouselViewport()) carousel.sync(); 5544 }, { once: true }); 5545 window.addEventListener("resize", window.ENISA.utils.debounce(function () { 5546 syncCarouselMode(); 5547 if (!isCarouselViewport()) { 5548 items.forEach(function (item) { 5549 item.style.minHeight = ""; 5550 }); 5551 lastSectorsPerPage = 0; 5552 dots.forEach(function (dot) { dot.hidden = false; }); 5553 carousel.reset(); 5554 return; 5555 } 5556 5557 syncCardHeights(); 5558 carousel.sync(); 5559 }, 150)); 5560 5561 return true; 5562 } 5563 5564 function watchSectionUntilReady(section) { 5565 if (!section || section.getAttribute(INIT_ATTR) === "true") return; 5566 if (section.getAttribute(WATCH_ATTR) === "true") return; 5567 section.setAttribute(WATCH_ATTR, "true"); 5568 5569 var observer = new MutationObserver(function () { 5570 if (section.getAttribute(INIT_ATTR) === "true") { 5571 observer.disconnect(); 5572 return; 5573 } 5574 if (initCarousel(section)) { 5575 observer.disconnect(); 5576 } 5577 }); 5578 5579 observer.observe(section, { childList: true, subtree: true }); 5580 5581 window.setTimeout(function () { 5582 if (section.getAttribute(INIT_ATTR) !== "true") { 5583 initCarousel(section); 5584 } 5585 observer.disconnect(); 5586 }, 4000); 5587 } 5588 5589 function init() { 5590 var sections = [].slice.call(document.querySelectorAll(SECTION_SELECTOR)); 5591 sections.forEach(function (section) { 5592 if (!initCarousel(section)) { 5593 watchSectionUntilReady(section); 5594 } 5595 }); 5596 } 5597 5598 if (document.readyState === "loading") { 5599 document.addEventListener("DOMContentLoaded", init); 5600 } else { 5601 init(); 5602 } 5603})(); 5604 5605 5606 5607 5608 5609/* 5610 ENISA | JS Module 5611 Modulo: 16-tabs-process 5612 Descripcion: Logica del modulo. 5613*/ 5614(function () { 5615 "use strict"; 5616 5617 var SECTION_SELECTOR = "[data-tabs-section]"; 5618 5619 function initProcessTabs(section) { 5620 if (!section || section.getAttribute("data-process-tabs-init") === "true") return; 5621 section.setAttribute("data-process-tabs-init", "true"); 5622 5623 var nav = section.querySelector("[data-tabs-nav]"); 5624 var panelsContainer = section.querySelector("[data-tabs-panels]"); 5625 var dotsContainer = section.querySelector("[data-tabs-dots]"); 5626 var mobileMediaQuery = window.matchMedia("(max-width: 575.98px)"); 5627 5628 if (!nav || !panelsContainer) return; 5629 5630 var tabs = [].slice.call(nav.querySelectorAll("[data-tab-index]")); 5631 var panels = [].slice.call(panelsContainer.querySelectorAll("[data-panel-index]")); 5632 var dots = dotsContainer 5633 ? [].slice.call(dotsContainer.querySelectorAll("[data-dot-index]")) 5634 : []; 5635 var mobileAnchors = []; 5636 var navLabelledBy = nav.getAttribute("aria-labelledby") || ""; 5637 var activeIndex = 0; 5638 5639 if (!tabs.length || tabs.length !== panels.length) return; 5640 5641 function isMobileLayout() { 5642 return mobileMediaQuery.matches; 5643 } 5644 5645 function ensureMobileAnchors() { 5646 if (mobileAnchors.length) return; 5647
5648 tabs.forEach(function (tab, index) { 5649 var anchor = document.createElement("div"); 5650 anchor.className = "enisa-process-tabs__mobile-panel-anchor"; 5651 anchor.setAttribute("data-mobile-panel-anchor", String(index)); 5652 tab.insertAdjacentElement("afterend", anchor); 5653 mobileAnchors.push(anchor); 5654 }); 5655 } 5656 5657 function syncPanelPlacement() { 5658 ensureMobileAnchors(); 5659 5660 var isMobile = mobileMediaQuery.matches; 5661 5662 panels.forEach(function (panel, index) { 5663 var targetParent = isMobile ? mobileAnchors[index] : panelsContainer; 5664 5665 if (targetParent && panel.parentNode !== targetParent) { 5666 targetParent.appendChild(panel); 5667 } 5668 }); 5669 5670 panelsContainer.hidden = isMobile; 5671 panelsContainer.setAttribute("aria-hidden", isMobile ? "true" : "false"); 5672 5673 mobileAnchors.forEach(function (anchor) { 5674 anchor.hidden = !isMobile; 5675 }); 5676 } 5677 5678 function syncAccessibilityState(index) { 5679 var isMobile = isMobileLayout(); 5680 5681 if (isMobile) { 5682 nav.removeAttribute("role"); 5683 nav.removeAttribute("aria-orientation"); 5684 if (navLabelledBy) { 5685 nav.removeAttribute("aria-labelledby"); 5686 } 5687 } else { 5688 nav.setAttribute("role", "tablist"); 5689 nav.setAttribute("aria-orientation", "vertical"); 5690 if (navLabelledBy) { 5691 nav.setAttribute("aria-labelledby", navLabelledBy); 5692 } 5693 } 5694
5695 tabs.forEach(function (tab, i) { 5696 var active = i === index; 5697 5698 if (isMobile) { 5699 tab.removeAttribute("role"); 5700 tab.removeAttribute("aria-selected"); 5701 tab.setAttribute("aria-expanded", active ? "true" : "false"); 5702 tab.setAttribute("tabindex", "0"); 5703 } else { 5704 tab.setAttribute("role", "tab"); 5705 tab.setAttribute("aria-selected", active ? "true" : "false"); 5706 tab.removeAttribute("aria-expanded"); 5707 tab.setAttribute("tabindex", active ? "0" : "-1"); 5708 } 5709 }); 5710 5711 panels.forEach(function (panel, i) { 5712 var active = i === index; 5713 5714 if (isMobile) { 5715 panel.removeAttribute("role"); 5716 } else { 5717 panel.setAttribute("role", "tabpanel"); 5718 } 5719 5720 panel.setAttribute("aria-labelledby", tabs[i].id); 5721 panel.setAttribute("aria-hidden", active ? "false" : "true"); 5722 panel.removeAttribute("tabindex"); 5723 }); 5724 } 5725 5726 function updateTabState(tab, active, isMobile) { 5727 tab.classList.toggle("enisa-process-tabs__item--active", active); 5728 5729 if (isMobile) { 5730 tab.removeAttribute("role"); 5731 tab.removeAttribute("aria-selected"); 5732 tab.setAttribute("aria-expanded", active ? "true" : "false"); 5733 tab.setAttribute("tabindex", "0"); 5734 } else { 5735 tab.setAttribute("role", "tab"); 5736 tab.setAttribute("aria-selected", active ? "true" : "false"); 5737 tab.removeAttribute("aria-expanded"); 5738 tab.setAttribute("tabindex", active ? "0" : "-1"); 5739 } 5740 } 5741 5742 function updatePanelState(panel, tab, active, isMobile) { 5743 if (isMobile) { 5744 panel.removeAttribute("role"); 5745 } else { 5746 panel.setAttribute("role", "tabpanel"); 5747 } 5748 5749 panel.setAttribute("aria-labelledby", tab.id); 5750 panel.setAttribute("aria-hidden", active ? "false" : "true"); 5751 panel.hidden = !active; 5752 panel.removeAttribute("tabindex"); 5753 } 5754 5755 function enhancePanelLinks() { 5756 panels.forEach(function (panel) { 5757 var linkRows = [].slice.call( 5758 panel.querySelectorAll(".enisa-transparencia-inline-links") 5759 ); 5760 5761 linkRows.forEach(function (row) { 5762 var links = [].slice.call(row.querySelectorAll("a")); 5763 5764 links.forEach(function (link) { 5765 if (!link || link.classList.contains("enisa-link")) return; 5766 5767 link.classList.add("enisa-link", "enisa-link--arrow"); 5768 5769 var label = document.createElement("span"); 5770 label.className = "enisa-link__label"; 5771 5772 while (link.firstChild) { 5773 label.appendChild(link.firstChild); 5774 } 5775 5776 var arrow = document.createElement("span"); 5777 arrow.className = "enisa-link__arrow enisa-icon enisa-icon--arrow-right"; 5778 arrow.setAttribute("aria-hidden", "true"); 5779 5780 link.appendChild(label); 5781 link.appendChild(arrow); 5782 }); 5783 }); 5784 }); 5785 } 5786 5787 function setActive(index, moveFocus) { 5788 if (index < 0 || index >= tabs.length) return; 5789 activeIndex = index; 5790 var isMobile = isMobileLayout(); 5791 5792 syncPanelPlacement(); 5793 syncAccessibilityState(activeIndex); 5794
5795 tabs.forEach(function (tab, i) { 5796 var active = i === index; 5797 updateTabState(tab, active, isMobile); 5798 5799 if (active) { 5800 section.setAttribute( 5801 "data-current-color", 5802 tab.getAttribute("data-tab-color") || "yellow" 5803 ); 5804 5805 if (moveFocus) { 5806 if (window.ENISA && window.ENISA.utils && typeof window.ENISA.utils.safeFocus === "function") { 5807 window.ENISA.utils.safeFocus(tab); 5808 } else { 5809 try { 5810 tab.focus({ preventScroll: true }); 5811 } catch (error) { 5812 tab.focus(); 5813 } 5814 } 5815 } 5816 } 5817 }); 5818 5819 panels.forEach(function (panel, i) { 5820 var active = i === activeIndex; 5821 5822 if ( 5823 !active && 5824 window.ENISA && 5825 window.ENISA.utils && 5826 typeof window.ENISA.utils.focusIfActiveInside === "function" 5827 ) { 5828 window.ENISA.utils.focusIfActiveInside(panel, tabs[activeIndex]); 5829 } else if ( 5830 !active && 5831 panel.contains(document.activeElement) && 5832 tabs[activeIndex] && 5833 typeof tabs[activeIndex].focus === "function" 5834 ) { 5835 if (window.ENISA && window.ENISA.utils && typeof window.ENISA.utils.safeFocus === "function") { 5836 window.ENISA.utils.safeFocus(tabs[activeIndex]); 5837 } else { 5838 try { 5839 tabs[activeIndex].focus({ preventScroll: true }); 5840 } catch (error) { 5841 tabs[activeIndex].focus(); 5842 } 5843 } 5844 } 5845 5846 updatePanelState(panel, tabs[i], active, isMobile); 5847 panel.classList.toggle("enisa-process-tabs__panel--active", active); 5848 5849 if (active) { 5850 panel.setAttribute("data-visible", ""); 5851 } else { 5852 panel.removeAttribute("data-visible"); 5853 } 5854 }); 5855 5856 dots.forEach(function (dot, i) { 5857 var active = i === activeIndex; 5858 dot.classList.toggle("enisa-process-tabs__dot--active", active); 5859 dot.setAttribute("aria-current", active ? "true" : "false"); 5860 }); 5861 } 5862 5863 function activateTab(index, moveFocus) { 5864 setActive(index, !!moveFocus); 5865 } 5866 5867 nav.addEventListener("click", function (e) { 5868 var tab = e.target.closest("[data-tab-index]"); 5869 if (!tab) return; 5870 5871 var index = Number(tab.getAttribute("data-tab-index")); 5872 if (isNaN(index)) return; 5873 5874 activateTab(index, false); 5875 }); 5876 5877 nav.addEventListener("keydown", function (e) { 5878 var tab = e.target.closest("[data-tab-index]"); 5879 if (!tab) return; 5880 5881 var current = Number(tab.getAttribute("data-tab-index")); 5882 if (isNaN(current)) return; 5883 5884 if (isMobileLayout()) { 5885 return; 5886 } 5887 5888 var next = current; 5889 5890 switch (e.key) { 5891 case "ArrowUp": 5892 case "ArrowLeft": 5893 next = current === 0 ? tabs.length - 1 : current - 1; 5894 break; 5895 case "ArrowDown": 5896 case "ArrowRight": 5897 next = current === tabs.length - 1 ? 0 : current + 1; 5898 break; 5899 case "Home": 5900 next = 0; 5901 break; 5902 case "End": 5903 next = tabs.length - 1; 5904 break; 5905 case "Enter": 5906 case " ": 5907 case "Spacebar": 5908 e.preventDefault(); 5909 activateTab(current, true); 5910 return; 5911 default: 5912 return; 5913 } 5914 5915 e.preventDefault(); 5916 activateTab(next, true); 5917 }); 5918 5919 if (dotsContainer) { 5920 dotsContainer.addEventListener("click", function (e) { 5921 var dot = e.target.closest("[data-dot-index]"); 5922 if (!dot) return; 5923 5924 var index = Number(dot.getAttribute("data-dot-index")); 5925 if (isNaN(index)) return; 5926 5927 activateTab(index, false); 5928 }); 5929 } 5930 5931 window.addEventListener("resize", function () { 5932 activateTab(activeIndex, false); 5933 }); 5934 5935 if (typeof mobileMediaQuery.addEventListener === "function") { 5936 mobileMediaQuery.addEventListener("change", function () { 5937 activateTab(activeIndex, false); 5938 }); 5939 } 5940
5941 window.addEventListener("enisa:transparency-tab-change", function () { 5942 activateTab(activeIndex, false); 5943 }); 5944 5945 enhancePanelLinks(); 5946 activateTab(0, false); 5947 } 5948 5949 function init() { 5950 var sections = document.querySelectorAll(SECTION_SELECTOR); 5951 if (!sections.length) return; 5952 5953 [].forEach.call(sections, initProcessTabs); 5954 } 5955 5956 if (document.readyState === "loading") { 5957 document.addEventListener("DOMContentLoaded", init); 5958 } else { 5959 init(); 5960 } 5961})(); 5962 5963/* 5964 ENISA | JS Module 5965 Modulo: 18-recursos-tabs 5966 Descripcion: Tabs de recursos/transparencia, paginacion, fallback de imagenes y ajuste de ratios. 5967*/ 5968(function () { 5969 "use strict"; 5970 5971 var ROOT_SELECTOR = 5972 "[data-recursos-tabs], .enisa-cmp-transparencia-tabs, .enisa-recursos-detalle__tabs"; 5973 var TAB_SELECTOR = "[data-resource-tab]"; 5974 var RECURSOS_DETALLE_SELECTOR = "[data-cmp-recursos-detalle]"; 5975 var PANEL_SELECTOR = "[data-resource-panel]"; 5976 var RESOURCE_ITEM_SELECTOR = "[data-resource-item]"; 5977 var RESOURCE_IMAGE_FALLBACK_SELECTOR = 5978 ".enisa-recursos-detalle__image[data-fallback-src]"; 5979 var RESOURCE_DETAIL_IMAGE_SELECTOR = ".enisa-recursos-detalle__image"; 5980 var RESOURCE_DETAIL_ITEM_SELECTOR = ".enisa-recursos-detalle__item"; 5981 var RESOURCE_DETAIL_MEDIA_SELECTOR = ".enisa-recursos-detalle__media"; 5982 5983 var TABS_INIT_ATTR = "data-recursos-tabs-anchor-init"; 5984 var LEGACY_TABS_INIT_ATTR = "data-recursos-tabs-init"; 5985 var PAGINATION_INIT_ATTR = "data-recursos-pagination-init"; 5986 var FALLBACK_INIT_ATTR = "data-recursos-image-fallback-init"; 5987 var RATIO_INIT_ATTR = "data-recursos-media-ratio-init"; 5988 5989 var hasInitialized = false; 5990 var hasAppliedInitialRequestedTab = false; 5991 var userHasInteractedWithResourceTabs = false; 5992 5993 var ANCHOR_ALIASES = { 5994 "logos-enisa": ["logos-enisa", "logo-enisa", "logotipos", "logotipos-enisa"], 5995 "sellos-clienteenisa": [ 5996 "sellos-clienteenisa", 5997 "sellos-cliente-enisa", 5998 "sellos-clientesenisa", 5999 "sello-clienteenisa", 6000 "sello-cliente-enisa", 6001 "selloclienteenisa", 6002 "clienteenisa", 6003 "clientes-enisa", 6004 "clientesenisa" 6005 ], 6006 "sellos-cliente-enisa": [ 6007 "sellos-cliente-enisa", 6008 "sellos-clienteenisa", 6009 "sellos-clientesenisa", 6010 "sello-clienteenisa", 6011 "sello-cliente-enisa", 6012 "selloclienteenisa", 6013 "clienteenisa", 6014 "clientes-enisa", 6015 "clientesenisa" 6016 ], 6017 "sellos-clientesenisa": [ 6018 "sellos-clientesenisa", 6019 "sellos-clienteenisa", 6020 "sellos-cliente-enisa", 6021 "sello-clienteenisa", 6022 "sello-cliente-enisa", 6023 "selloclienteenisa", 6024 "clienteenisa", 6025 "clientes-enisa", 6026 "clientesenisa" 6027 ], 6028 "sellos-empresa-emergente-soystartup": [ 6029 "sellos-empresa-emergente-soystartup", 6030 "sello-empresa-emergente-soystartup", 6031 "empresa-emergente-soystartup", 6032 "empresa-emergente", 6033 "soystartup" 6034 ], 6035 "sello-financiada-por-enisa": [ 6036 "sello-financiada-por-enisa", 6037 "sello-financiador-enisa", 6038 "sello-financiadapor-enisa", 6039 "sello-financiadaporenisa" 6040 ], 6041 "sello-financiador-enisa": [ 6042 "sello-financiador-enisa", 6043 "sello-financiada-por-enisa", 6044 "sello-financiadapor-enisa", 6045 "sello-financiadaporenisa" 6046 ], 6047 "sello-financiadapor-enisa": [ 6048 "sello-financiadapor-enisa", 6049 "sello-financiada-por-enisa", 6050 "sello-financiador-enisa", 6051 "sello-financiadaporenisa" 6052 ], 6053 "sello-financiadaporenisa": [ 6054 "sello-financiadaporenisa", 6055 "sello-financiada-por-enisa", 6056 "sello-financiador-enisa", 6057 "sello-financiadapor-enisa" 6058 ] 6059 }; 6060 6061 function prefersReducedMotion() { 6062 return !!( 6063 window.matchMedia && 6064 window.matchMedia("(prefers-reduced-motion: reduce)").matches 6065 ); 6066 } 6067 6068 function debounce(fn, delay) { 6069 var timer = null; 6070 6071 return function () { 6072 var context = this; 6073 var args = arguments; 6074 6075 clearTimeout(timer); 6076 timer = setTimeout(function () { 6077 fn.apply(context, args); 6078 }, delay || 150); 6079 }; 6080 } 6081 6082 function afterNextPaint(callback) { 6083 window.requestAnimationFrame(function () { 6084 window.requestAnimationFrame(callback); 6085 }); 6086 } 6087 6088 function normalizeAnchor(value) { 6089 var text = String(value || "").replace(/^#/, "").trim(); 6090 6091 try { 6092 text = decodeURIComponent(text); 6093 } catch (error) {} 6094 6095 return text 6096 .toLowerCase() 6097 .replace(/&/g, " ") 6098 .replace(/&/g, " ") 6099 .replace(/á/g, "a") 6100 .replace(/é/g, "e") 6101 .replace(/ÃÂ/g, "i") 6102 .replace(/ó/g, "o") 6103 .replace(/ú/g, "u") 6104 .replace(/ñ/g, "n") 6105 .replace(/[áà äâ]/g, "a") 6106 .replace(/[éèëê]/g, "e") 6107 .replace(/[Ãìïî]/g, "i") 6108 .replace(/[óòöô]/g, "o") 6109 .replace(/[úùüû]/g, "u") 6110 .replace(/ñ/g, "n") 6111 .replace(/ç/g, "c") 6112 .replace(/#/g, "") 6113 .replace(/[^\w\s-]/g, "") 6114 .replace(/_/g, "-") 6115 .replace(/\s+/g, "-") 6116 .replace(/-+/g, "-") 6117 .replace(/^-|-$/g, ""); 6118 } 6119 6120 function getQueryParam(name) { 6121 if (!window.URLSearchParams) return ""; 6122 6123 var params = new URLSearchParams(window.location.search || ""); 6124 return params.get(name) || ""; 6125 } 6126 6127 function getRequestedTabAnchor() { 6128 return getQueryParam("tab") || ""; 6129 } 6130 6131 function getAnchorCandidates(value) { 6132 var cleanValue = normalizeAnchor(value); 6133 var candidates = []; 6134 6135 if (!cleanValue) return candidates; 6136 6137 candidates.push(cleanValue); 6138 6139 if (ANCHOR_ALIASES[cleanValue]) { 6140 candidates = candidates.concat(ANCHOR_ALIASES[cleanValue]); 6141 } 6142
6143 Object.keys(ANCHOR_ALIASES).forEach(function (key) { 6144 if (ANCHOR_ALIASES[key].indexOf(cleanValue) !== -1) { 6145 candidates.push(key); 6146 candidates = candidates.concat(ANCHOR_ALIASES[key]); 6147 } 6148 }); 6149 6150 return candidates 6151 .map(normalizeAnchor) 6152 .filter(function (candidate, index, list) { 6153 return candidate && list.indexOf(candidate) === index; 6154 }); 6155 } 6156 6157 function matchesAnchor(requestedValue, elementValue) { 6158 var requestedCandidates = getAnchorCandidates(requestedValue); 6159 var elementCandidates = getAnchorCandidates(elementValue); 6160 6161 if (!requestedCandidates.length || !elementCandidates.length) return false; 6162 6163 return requestedCandidates.some(function (requestedCandidate) { 6164 return elementCandidates.indexOf(requestedCandidate) !== -1; 6165 }); 6166 } 6167 6168 function focusTab(tab) { 6169 if (!tab) return; 6170 6171 if ( 6172 window.ENISA && 6173 window.ENISA.utils && 6174 typeof window.ENISA.utils.safeFocus === "function" 6175 ) { 6176 window.ENISA.utils.safeFocus(tab); 6177 return; 6178 } 6179 6180 try { 6181 tab.focus({ preventScroll: true }); 6182 } catch (error) {} 6183 } 6184 6185 function setPanelTabbables(panel, isActive) { 6186 if (!panel) return; 6187 6188 if ( 6189 window.ENISA && 6190 window.ENISA.utils && 6191 typeof window.ENISA.utils.setSubtreeTabbable === "function" 6192 ) { 6193 window.ENISA.utils.setSubtreeTabbable(panel, isActive); 6194 } 6195 } 6196 6197 function getTabGeneratedAnchor(tab) { 6198 if (!tab) return ""; 6199 6200 return normalizeAnchor( 6201 tab.getAttribute("data-resource-anchor") || 6202 tab.getAttribute("data-anchor") || 6203 tab.getAttribute("id") || 6204 tab.getAttribute("aria-controls") || 6205 tab.textContent || 6206 "" 6207 ); 6208 } 6209 6210 function getPanelGeneratedAnchor(panel) { 6211 if (!panel) return ""; 6212 6213 return normalizeAnchor( 6214 panel.getAttribute("data-resource-anchor") || 6215 panel.getAttribute("id") || 6216 panel.getAttribute("aria-labelledby") || 6217 "" 6218 ); 6219 } 6220 6221 function hydrateMissingAnchors(tabs, panels) {
6222 tabs.forEach(function (tab, index) { 6223 var currentTabAnchor = normalizeAnchor(tab.getAttribute("data-resource-anchor")); 6224 var generatedTabAnchor = normalizeAnchor(tab.textContent || tab.getAttribute("id") || ""); 6225 var panel = panels[index]; 6226 var currentPanelAnchor; 6227 var generatedPanelAnchor; 6228 6229 if (!currentTabAnchor && generatedTabAnchor) { 6230 tab.setAttribute("data-resource-anchor", generatedTabAnchor); 6231 } 6232 6233 if (!panel) return; 6234 6235 currentPanelAnchor = normalizeAnchor(panel.getAttribute("data-resource-anchor")); 6236 generatedPanelAnchor = normalizeAnchor( 6237 currentTabAnchor || generatedTabAnchor || panel.getAttribute("id") || "" 6238 ); 6239 6240 if (!currentPanelAnchor && generatedPanelAnchor) { 6241 panel.setAttribute("data-resource-anchor", generatedPanelAnchor); 6242 } 6243 }); 6244 } 6245 6246 function getIndexFromAnchor(tabs, panels, requestedAnchor) { 6247 var cleanRequested = normalizeAnchor(requestedAnchor); 6248 6249 if (!cleanRequested) return -1; 6250 6251 for (var i = 0; i < tabs.length; i += 1) { 6252 var tab = tabs[i]; 6253 var panel = panels[i]; 6254 6255 if ( 6256 matchesAnchor(cleanRequested, tab.getAttribute("data-resource-anchor")) || 6257 matchesAnchor(cleanRequested, tab.textContent) || 6258 matchesAnchor(cleanRequested, tab.getAttribute("id")) || 6259 matchesAnchor(cleanRequested, tab.getAttribute("aria-controls")) || 6260 matchesAnchor(cleanRequested, getTabGeneratedAnchor(tab)) || 6261 matchesAnchor(cleanRequested, panel ? panel.getAttribute("data-resource-anchor") : "") || 6262 matchesAnchor(cleanRequested, panel ? panel.getAttribute("id") : "") || 6263 matchesAnchor(cleanRequested, panel ? panel.getAttribute("aria-labelledby") : "") || 6264 matchesAnchor(cleanRequested, getPanelGeneratedAnchor(panel)) 6265 ) { 6266 return i; 6267 } 6268 } 6269 6270 return -1; 6271 } 6272 6273 function dispatchCustomEvent(target, eventName, detail) { 6274 var event; 6275 6276 if (!target) return; 6277 6278 if (typeof window.CustomEvent === "function") { 6279 event = new CustomEvent(eventName, { 6280 bubbles: true, 6281 detail: detail || {} 6282 }); 6283 } else { 6284 event = document.createEvent("CustomEvent"); 6285 event.initCustomEvent(eventName, true, false, detail || {}); 6286 } 6287 6288 target.dispatchEvent(event); 6289 } 6290 6291 function setTabState(root, tabs, panels, index, options) { 6292 var opts = options || {}; 6293 6294 if (index < 0 || index >= tabs.length) return; 6295
6296 tabs.forEach(function (tab, tabIndex) { 6297 var isActive = tabIndex === index; 6298 6299 tab.classList.toggle("is-active", isActive); 6300 tab.setAttribute("aria-selected", isActive ? "true" : "false"); 6301 tab.setAttribute("tabindex", isActive ? "0" : "-1"); 6302 6303 if (!normalizeAnchor(tab.getAttribute("data-resource-anchor"))) { 6304 tab.setAttribute("data-resource-anchor", normalizeAnchor(tab.textContent)); 6305 } 6306 6307 if (isActive && opts.moveFocus) { 6308 focusTab(tab); 6309 } 6310 }); 6311 6312 panels.forEach(function (panel, panelIndex) { 6313 var isActive = panelIndex === index; 6314 6315 if ( 6316 !isActive && 6317 panel.contains(document.activeElement) && 6318 tabs[index] && 6319 typeof tabs[index].focus === "function" 6320 ) { 6321 focusTab(tabs[index]); 6322 } 6323 6324 panel.classList.toggle("is-active", isActive); 6325 panel.hidden = !isActive; 6326 panel.setAttribute("aria-hidden", isActive ? "false" : "true"); 6327 panel.removeAttribute("tabindex"); 6328 setPanelTabbables(panel, isActive); 6329 6330 if (!normalizeAnchor(panel.getAttribute("data-resource-anchor")) && tabs[panelIndex]) { 6331 panel.setAttribute("data-resource-anchor", normalizeAnchor(tabs[panelIndex].textContent)); 6332 } 6333 6334 if (isActive) { 6335 dispatchCustomEvent(panel, "enisa:resource-tab-visible", { 6336 tab: tabs[index], 6337 panel: panel 6338 }); 6339 } 6340 }); 6341 6342 dispatchCustomEvent(root, "enisa:resource-tab-change", { 6343 tab: tabs[index], 6344 panel: panels[index] 6345 }); 6346 } 6347 6348 function isTabAlreadyActive(tabs, panels, index) { 6349 return !!( 6350 tabs[index] && 6351 panels[index] && 6352 tabs[index].classList.contains("is-active") && 6353 tabs[index].getAttribute("aria-selected") === "true" && 6354 panels[index].classList.contains("is-active") && 6355 panels[index].hidden === false && 6356 panels[index].getAttribute("aria-hidden") === "false" 6357 ); 6358 } 6359 6360 function initRecursosTabs(root) { 6361 if (!root || root.getAttribute(TABS_INIT_ATTR) === "true") return; 6362 6363 root.setAttribute(TABS_INIT_ATTR, "true"); 6364 root.setAttribute(LEGACY_TABS_INIT_ATTR, "true"); 6365 6366 var tabs = [].slice.call(root.querySelectorAll(TAB_SELECTOR)); 6367 if (!tabs.length) return; 6368 6369 var panels = tabs 6370 .map(function (tab) { 6371 var id = tab.getAttribute("aria-controls"); 6372 return id ? document.getElementById(id) : null; 6373 }) 6374 .filter(Boolean); 6375 6376 if (!panels.length) return; 6377 6378 hydrateMissingAnchors(tabs, panels); 6379 6380 var isTransparencyTabs = root.classList.contains("enisa-cmp-transparencia-tabs"); 6381 var isRecursosDetalleTabs = !!root.closest(RECURSOS_DETALLE_SELECTOR); 6382 var tabsNavInner = isTransparencyTabs 6383 ? root.querySelector(".enisa-cmp-transparencia-tabs__nav-inner") 6384 : root; 6385 var requested = parseInt(root.getAttribute("data-active-tab"), 10); 6386 var activeIndex = isTransparencyTabs 6387 ? 0 6388 : isNaN(requested) 6389 ? 0 6390 : Math.min(Math.max(requested - 1, 0), tabs.length - 1); 6391 6392 function notifyLayoutRefresh() { 6393 if (!isTransparencyTabs) return; 6394 6395 afterNextPaint(function () { 6396 window.dispatchEvent(new Event("enisa:transparency-tab-change")); 6397 }); 6398 } 6399 6400 function alignActiveTabToStart(behavior) { 6401 var activeTab; 6402 var targetLeft; 6403 6404 if (!tabsNavInner) return; 6405 if (tabsNavInner.scrollWidth <= tabsNavInner.clientWidth) return; 6406 6407 activeTab = tabs[activeIndex]; 6408 if (!activeTab) return; 6409 6410 targetLeft = Math.max(0, activeTab.offsetLeft - tabsNavInner.offsetLeft); 6411 6412 tabsNavInner.scrollTo({ 6413 left: targetLeft, 6414 behavior: prefersReducedMotion() ? "auto" : behavior || "auto" 6415 }); 6416 } 6417 6418 function setActive(index, options) { 6419 var opts = options || {}; 6420 6421 if (index < 0 || index >= tabs.length) return; 6422 6423 activeIndex = index; 6424 6425 if (!isTabAlreadyActive(tabs, panels, activeIndex)) { 6426 setTabState(root, tabs, panels, activeIndex, opts); 6427 } 6428 6429 alignActiveTabToStart(opts.moveFocus ? "smooth" : "auto"); 6430 notifyLayoutRefresh(); 6431 } 6432 6433 function activateFromRequestedAnchor(options) { 6434 var requestedAnchor; 6435 var requestedIndex; 6436
6437 if (!isRecursosDetalleTabs) return false; 6438 6439 requestedAnchor = getRequestedTabAnchor(); 6440 requestedIndex = getIndexFromAnchor(tabs, panels, requestedAnchor); 6441 6442 if (requestedIndex === -1) return false; 6443 6444 setActive(requestedIndex, { 6445 moveFocus: false, 6446 scroll: !!(options && options.scroll), 6447 scrollBehavior: options && options.scrollBehavior ? options.scrollBehavior : "smooth" 6448 }); 6449 6450 return true; 6451 } 6452 6453 tabs.forEach(function (tab, i) { 6454 tab.addEventListener("click", function (event) { 6455 event.preventDefault(); 6456 userHasInteractedWithResourceTabs = true; 6457 6458 setActive(i, { 6459 moveFocus: false 6460 }); 6461 }); 6462 6463 tab.addEventListener("keydown", function (event) { 6464 var next = activeIndex; 6465 6466 switch (event.key) { 6467 case "ArrowRight": 6468 case "ArrowDown": 6469 next = (activeIndex + 1) % tabs.length; 6470 break; 6471 case "ArrowLeft": 6472 case "ArrowUp": 6473 next = (activeIndex - 1 + tabs.length) % tabs.length; 6474 break; 6475 case "Home": 6476 next = 0; 6477 break; 6478 case "End": 6479 next = tabs.length - 1; 6480 break; 6481 default: 6482 return; 6483 } 6484 6485 event.preventDefault(); 6486 userHasInteractedWithResourceTabs = true; 6487 6488 setActive(next, { 6489 moveFocus: true, 6490 scroll: false 6491 }); 6492 }); 6493 }); 6494 6495 if (!activateFromRequestedAnchor({ scroll: false })) { 6496 setActive(activeIndex, { 6497 moveFocus: false, 6498 scroll: false 6499 }); 6500 } 6501 } 6502 6503 function initRecursosDetallePagination(componentRoot) { 6504 if (!componentRoot || componentRoot.getAttribute(PAGINATION_INIT_ATTR) === "true") { 6505 return; 6506 } 6507 6508 componentRoot.setAttribute(PAGINATION_INIT_ATTR, "true"); 6509 6510 var pageSize = parseInt(componentRoot.getAttribute("data-items-per-page"), 10); 6511 if (isNaN(pageSize) || pageSize < 1) pageSize = 10; 6512 if (pageSize > 10) pageSize = 10; 6513 6514 var tabs = [].slice.call(componentRoot.querySelectorAll(TAB_SELECTOR)); 6515 var panels = [].slice.call(componentRoot.querySelectorAll(PANEL_SELECTOR)); 6516 if (!panels.length) return; 6517 6518 panels.forEach(function (panel) { 6519 var items = [].slice.call(panel.querySelectorAll(RESOURCE_ITEM_SELECTOR)); 6520 var pagination = panel.querySelector("[data-resource-pagination]"); 6521 var currentPage = 1; 6522 6523 if (!items.length) { 6524 if (pagination) pagination.setAttribute("hidden", ""); 6525 return; 6526 } 6527 6528 function renderPagination(totalPages) { 6529 var windowSize = 3; 6530 var half = Math.floor(windowSize / 2); 6531 var start = Math.max(1, currentPage - half); 6532 var end = Math.min(totalPages, start + windowSize - 1); 6533 var prevBtn; 6534 var pageList; 6535 var nextBtn; 6536 var i; 6537 6538 if (!pagination) return; 6539 6540 pagination.innerHTML = ""; 6541 6542 if (totalPages <= 1) { 6543 pagination.setAttribute("hidden", ""); 6544 return; 6545 } 6546 6547 pagination.removeAttribute("hidden"); 6548 6549 if (end - start < windowSize - 1) { 6550 start = Math.max(1, end - windowSize + 1); 6551 } 6552 6553 prevBtn = document.createElement("button"); 6554 prevBtn.type = "button"; 6555 prevBtn.className = "enisa-pagination__btn enisa-pagination__btn--arrow is-prev"; 6556 prevBtn.setAttribute("aria-label", "Página anterior"); 6557 prevBtn.innerHTML = 6558 '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 6559 prevBtn.disabled = currentPage === 1; 6560 prevBtn.addEventListener("click", function () { 6561 if (currentPage <= 1) return; 6562 6563 currentPage -= 1; 6564 render({ type: "prev" }); 6565 }); 6566 pagination.appendChild(prevBtn); 6567 6568 pageList = document.createElement("div"); 6569 pageList.className = "enisa-pagination__list"; 6570 6571 for (i = start; i <= end; i += 1) { 6572 (function (pageNumber) { 6573 var pageBtn = document.createElement("button"); 6574 var isCurrentPage = pageNumber === currentPage; 6575 6576 pageBtn.type = "button"; 6577 pageBtn.className = "enisa-pagination__btn" + (isCurrentPage ? " is-active" : ""); 6578 pageBtn.textContent = String(pageNumber); 6579 pageBtn.setAttribute( 6580 "aria-label", 6581 isCurrentPage 6582 ? "Página " + pageNumber + ", actual" 6583 : "Ir a la página " + pageNumber 6584 ); 6585 6586 if (isCurrentPage) { 6587 pageBtn.setAttribute("aria-current", "page"); 6588 } 6589 6590 pageBtn.addEventListener("click", function () { 6591 if (currentPage === pageNumber) return; 6592 6593 currentPage = pageNumber; 6594 render({ type: "page", page: pageNumber }); 6595 }); 6596 6597 pageList.appendChild(pageBtn); 6598 })(i); 6599 } 6600 6601 pagination.appendChild(pageList); 6602 6603 nextBtn = document.createElement("button"); 6604 nextBtn.type = "button"; 6605 nextBtn.className = "enisa-pagination__btn enisa-pagination__btn--arrow is-next"; 6606 nextBtn.setAttribute("aria-label", "Página siguiente"); 6607 nextBtn.innerHTML = 6608 '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 6609 nextBtn.disabled = currentPage === totalPages; 6610 nextBtn.addEventListener("click", function () { 6611 if (currentPage >= totalPages) return; 6612 6613 currentPage += 1; 6614 render({ type: "next" }); 6615 }); 6616 pagination.appendChild(nextBtn); 6617 } 6618 6619 function getPaginationFocusTarget(focusTarget) { 6620 if (!focusTarget || !pagination) return null; 6621 6622 if (focusTarget.type === "prev") { 6623 return pagination.querySelector(".enisa-pagination__btn--arrow.is-prev"); 6624 } 6625 6626 if (focusTarget.type === "next") { 6627 return pagination.querySelector(".enisa-pagination__btn--arrow.is-next"); 6628 } 6629 6630 if (focusTarget.type === "page") { 6631 return ( 6632 [].slice 6633 .call(pagination.querySelectorAll(".enisa-pagination__list .enisa-pagination__btn")) 6634 .filter(function (button) { 6635 return button.textContent === String(focusTarget.page); 6636 })[0] || null 6637 ); 6638 } 6639 6640 return null; 6641 } 6642 6643 function render(focusTarget) { 6644 var totalPages = Math.max(1, Math.ceil(items.length / pageSize)); 6645 var activeItem = 6646 document.activeElement && document.activeElement.closest 6647 ? document.activeElement.closest(RESOURCE_ITEM_SELECTOR) 6648 : null; 6649 var start; 6650 var end; 6651 6652 if (currentPage > totalPages) currentPage = totalPages; 6653 if (currentPage < 1) currentPage = 1; 6654 6655 start = (currentPage - 1) * pageSize; 6656 end = start + pageSize; 6657 6658 if ( 6659 activeItem && 6660 items.indexOf(activeItem) > -1 && 6661 (items.indexOf(activeItem) < start || items.indexOf(activeItem) >= end) 6662 ) { 6663 var fallbackFocus = 6664 getPaginationFocusTarget(focusTarget) || 6665 tabs.filter(function (tab) { 6666 return tab.getAttribute("aria-selected") === "true"; 6667 })[0] || 6668 tabs[0]; 6669 6670 if ( 6671 fallbackFocus && 6672 window.ENISA && 6673 window.ENISA.utils && 6674 typeof window.ENISA.utils.focusWithoutScroll === "function" 6675 ) { 6676 window.ENISA.utils.focusWithoutScroll(fallbackFocus); 6677 } else if (fallbackFocus && typeof fallbackFocus.focus === "function") { 6678 focusTab(fallbackFocus); 6679 } 6680 } 6681
6682 items.forEach(function (item, index) { 6683 item.hidden = index < start || index >= end; 6684 }); 6685 6686 renderPagination(totalPages); 6687 6688 if (focusTarget) { 6689 afterNextPaint(function () { 6690 var nextFocusTarget = getPaginationFocusTarget(focusTarget); 6691 6692 if ( 6693 nextFocusTarget && 6694 typeof nextFocusTarget.focus === "function" && 6695 document.contains(nextFocusTarget) 6696 ) { 6697 focusTab(nextFocusTarget); 6698 } 6699 }); 6700 } 6701 } 6702 6703 panel.addEventListener("enisa:resource-tab-visible", function () { 6704 currentPage = 1; 6705 render(); 6706 }); 6707 6708 render(); 6709 }); 6710 } 6711 6712 function initResourceImageFallbacks(scope) { 6713 var root = scope && scope.querySelectorAll ? scope : document; 6714 var images = root.querySelectorAll(RESOURCE_IMAGE_FALLBACK_SELECTOR); 6715 6716 [].forEach.call(images, function (image) { 6717 if (!image || image.getAttribute(FALLBACK_INIT_ATTR) === "true") return; 6718 6719 image.setAttribute(FALLBACK_INIT_ATTR, "true"); 6720 image.addEventListener("error", function () { 6721 var fallbackSrc = image.getAttribute("data-fallback-src"); 6722 6723 if (!fallbackSrc) return; 6724 6725 image.removeAttribute("data-fallback-src"); 6726 6727 if (image.getAttribute("src") === fallbackSrc || image.src === fallbackSrc) { 6728 return; 6729 } 6730 6731 image.src = fallbackSrc; 6732 }); 6733 }); 6734 } 6735 6736 function clearResourceMediaRatioClasses(item, media) { 6737 var classes = [ 6738 "enisa-recursos-detalle__item--media-portrait", 6739 "enisa-recursos-detalle__item--media-square", 6740 "enisa-recursos-detalle__item--media-landscape", 6741 "enisa-recursos-detalle__item--media-panorama", 6742 "enisa-recursos-detalle__media--portrait", 6743 "enisa-recursos-detalle__media--square", 6744 "enisa-recursos-detalle__media--landscape", 6745 "enisa-recursos-detalle__media--panorama" 6746 ]; 6747 6748 classes.forEach(function (className) { 6749 if (item) item.classList.remove(className); 6750 if (media) media.classList.remove(className); 6751 }); 6752 } 6753 6754 function getResourceItemText(image, item) { 6755 var title = item.querySelector(".enisa-recursos-detalle__title"); 6756 var downloadLabel = item.querySelector(".enisa-recursos-detalle__download-label"); 6757 6758 return [ 6759 image.getAttribute("alt") || "", 6760 image.getAttribute("src") || "", 6761 title ? title.textContent : "", 6762 downloadLabel ? downloadLabel.textContent : "" 6763 ] 6764 .join(" ") 6765 .toLowerCase(); 6766 } 6767 6768 function applyResourceMediaRatioClass(image) { 6769 var media; 6770 var item; 6771 var itemText; 6772 var width; 6773 var height; 6774 var ratio; 6775 var type = "landscape"; 6776 6777 if (!image) return; 6778 6779 media = image.closest(RESOURCE_DETAIL_MEDIA_SELECTOR); 6780 item = image.closest(RESOURCE_DETAIL_ITEM_SELECTOR); 6781 6782 if (!media || !item) return; 6783 6784 itemText = getResourceItemText(image, item); 6785 6786 if ( 6787 media.classList.contains("enisa-recursos-detalle__media--conjunto-asset") || 6788 image.classList.contains("enisa-recursos-detalle__image--conjunto-asset") || 6789 itemText.indexOf("conjunto") !== -1 6790 ) { 6791 clearResourceMediaRatioClasses(item, media); 6792 6793 item.classList.add("enisa-recursos-detalle__item--media-landscape"); 6794 item.classList.add("enisa-recursos-detalle__item--conjunto-asset"); 6795 6796 media.classList.add("enisa-recursos-detalle__media--landscape"); 6797 media.classList.add("enisa-recursos-detalle__media--conjunto-asset"); 6798 media.setAttribute("data-media-ratio", "landscape"); 6799 6800 image.classList.add("enisa-recursos-detalle__image--conjunto-asset"); 6801 6802 return; 6803 } 6804 6805 width = image.naturalWidth || 0; 6806 height = image.naturalHeight || 0; 6807 6808 if (!width || !height) return; 6809 6810 ratio = width / height; 6811 6812 if (ratio < 0.85) { 6813 type = "portrait"; 6814 } else if (ratio <= 1.2) { 6815 type = "square"; 6816 } else if (ratio >= 2.2) { 6817 type = "panorama"; 6818 } 6819 6820 clearResourceMediaRatioClasses(item, media); 6821 item.classList.add("enisa-recursos-detalle__item--media-" + type); 6822 media.classList.add("enisa-recursos-detalle__media--" + type); 6823 media.setAttribute("data-media-ratio", type); 6824 } 6825 6826 function initResourceMediaRatios(scope) { 6827 var root = scope && scope.querySelectorAll ? scope : document; 6828 var images = root.querySelectorAll(RESOURCE_DETAIL_IMAGE_SELECTOR); 6829 6830 [].forEach.call(images, function (image) { 6831 if (!image) return; 6832
6833 if (image.complete && image.naturalWidth) { 6834 applyResourceMediaRatioClass(image); 6835 } 6836 6837 if (image.getAttribute(RATIO_INIT_ATTR) === "true") return; 6838 6839 image.setAttribute(RATIO_INIT_ATTR, "true"); 6840 6841 image.addEventListener("load", function () { 6842 applyResourceMediaRatioClass(image); 6843 }); 6844 }); 6845 } 6846 6847 function applyRequestedTab(options) { 6848 var opts = options || {}; 6849 var requestedAnchor = getRequestedTabAnchor(); 6850 var roots; 6851 var applied = false; 6852 6853 if (!normalizeAnchor(requestedAnchor)) return false; 6854 if (userHasInteractedWithResourceTabs && !opts.force) return false; 6855 6856 roots = [].slice.call(document.querySelectorAll(ROOT_SELECTOR)); 6857 6858 roots.forEach(function (root) { 6859 var isRecursosDetalleTabs = !!root.closest(RECURSOS_DETALLE_SELECTOR); 6860 var tabs; 6861 var panels; 6862 var index; 6863 6864 if (!isRecursosDetalleTabs) return; 6865 6866 tabs = [].slice.call(root.querySelectorAll(TAB_SELECTOR)); 6867 if (!tabs.length) return; 6868 6869 panels = tabs 6870 .map(function (tab) { 6871 var id = tab.getAttribute("aria-controls"); 6872 return id ? document.getElementById(id) : null; 6873 }) 6874 .filter(Boolean); 6875 6876 if (!panels.length) return; 6877 6878 hydrateMissingAnchors(tabs, panels); 6879 index = getIndexFromAnchor(tabs, panels, requestedAnchor); 6880 6881 if (index < 0) return; 6882 6883 if (!isTabAlreadyActive(tabs, panels, index)) { 6884 setTabState(root, tabs, panels, index, { moveFocus: false }); 6885 } 6886 6887 applied = true; 6888 hasAppliedInitialRequestedTab = true; 6889 }); 6890 6891 return applied; 6892 } 6893 6894 function initScope(scope) { 6895 var root = scope && scope.querySelectorAll ? scope : document; 6896 var tabRoots = []; 6897 var recursosDetalleRoots = []; 6898 6899 if (root.matches && root.matches(ROOT_SELECTOR)) { 6900 tabRoots.push(root); 6901 } 6902 6903 tabRoots = tabRoots.concat([].slice.call(root.querySelectorAll(ROOT_SELECTOR))); 6904 tabRoots.forEach(initRecursosTabs); 6905 6906 if (root.matches && root.matches(RECURSOS_DETALLE_SELECTOR)) { 6907 recursosDetalleRoots.push(root); 6908 } 6909 6910 recursosDetalleRoots = recursosDetalleRoots.concat( 6911 [].slice.call(root.querySelectorAll(RECURSOS_DETALLE_SELECTOR)) 6912 ); 6913 6914 recursosDetalleRoots.forEach(function (componentRoot) { 6915 initRecursosDetallePagination(componentRoot); 6916 initResourceImageFallbacks(componentRoot); 6917 initResourceMediaRatios(componentRoot); 6918 }); 6919 6920 initResourceImageFallbacks(root); 6921 initResourceMediaRatios(root); 6922 } 6923 6924 function init() { 6925 if (hasInitialized) { 6926 applyRequestedTab({ force: false, scroll: false }); 6927 return; 6928 } 6929 6930 hasInitialized = true; 6931 initScope(document); 6932 applyRequestedTab({ force: true, scroll: false }); 6933 } 6934 6935 if (document.readyState === "loading") { 6936 document.addEventListener("DOMContentLoaded", init); 6937 } else { 6938 init(); 6939 } 6940 6941 window.addEventListener("load", function () { 6942 if (hasAppliedInitialRequestedTab || userHasInteractedWithResourceTabs) return; 6943 applyRequestedTab({ force: false, scroll: false }); 6944 }); 6945 6946 window.addEventListener("popstate", function () { 6947 userHasInteractedWithResourceTabs = false; 6948 applyRequestedTab({ force: true, scroll: false }); 6949 }); 6950 6951 window.addEventListener( 6952 "resize", 6953 debounce(function () { 6954 initResourceMediaRatios(document); 6955 }, 150) 6956 ); 6957 6958 if ("MutationObserver" in window) { 6959 var mutationTimer = null; 6960 var mutationRoot = 6961 document.querySelector(".page-layout__content") || 6962 document.querySelector("#main-content") || 6963 document.querySelector("#main-content") || 6964 document.body; 6965 6966 var observer = new MutationObserver(function (mutations) { 6967 var addedNodes = []; 6968 6969 mutations.forEach(function (mutation) { 6970 if (!mutation.addedNodes || !mutation.addedNodes.length) return; 6971 6972 [].forEach.call(mutation.addedNodes, function (node) { 6973 if (node && node.nodeType === 1) { 6974 addedNodes.push(node); 6975 } 6976 }); 6977 }); 6978 6979 if (!addedNodes.length) return; 6980 6981 clearTimeout(mutationTimer); 6982 mutationTimer = setTimeout(function () {
6983 addedNodes.forEach(initScope); 6984 applyRequestedTab({ force: false, scroll: false }); 6985 }, 150); 6986 }); 6987 6988 if (mutationRoot) { 6989 observer.observe(mutationRoot, { 6990 childList: true, 6991 subtree: true 6992 }); 6993 } 6994 } 6995})(); 6996 6997/* 6998 ENISA | JS Module 6999 Modulo: 19-transparency 7000 Descripcion: Logica del modulo de transparencia. 7001*/ 7002(function () { 7003 "use strict"; 7004 7005 var TABLE_SELECTOR = "[data-transparency-table], .enisa-transparency-table"; 7006 var TRANSPARENCY_SCOPE_SELECTOR = 7007 "[data-transparency-documents], .enisa-cmp-transparencia-documents, .enisa-transparencia-documents"; 7008 var RESOURCE_DETAIL_SELECTOR = ".enisa-recursos-detalle--transparencia"; 7009 var SELECT_SELECTOR = "[data-enisa-select], .enisa-select-control"; 7010 var RESOURCE_ITEM_SELECTOR = 7011 ".enisa-recursos-detalle__item:not(.enisa-recursos-detalle__item--heading)"; 7012 var RESOURCE_LIST_SELECTOR = ".enisa-recursos-detalle__list"; 7013 var YEAR_FILTER_ATTR = "data-transparency-year"; 7014 var INIT_SELECT_ATTR = "data-enisa-select-init"; 7015 var INIT_TABLE_ATTR = "data-transparency-init"; 7016 var INIT_DETAIL_ATTR = "data-transparency-detail-init"; 7017 var DETAIL_PAGE_SIZE = 10; 7018 7019 var MONTHS_ES = { 7020 enero: 0, 7021 febrero: 1, 7022 marzo: 2, 7023 abril: 3, 7024 mayo: 4, 7025 junio: 5, 7026 julio: 6, 7027 agosto: 7, 7028 septiembre: 8, 7029 setiembre: 8, 7030 octubre: 9, 7031 noviembre: 10, 7032 diciembre: 11 7033 }; 7034 7035 function normalizeText(value) { 7036 return (value || "").toString().replace(/\s+/g, " ").trim(); 7037 } 7038 7039 function normalizeFilenameLabel(value) { 7040 return normalizeText(value).replace(/_/g, " "); 7041 } 7042 7043 function getTextContent(node) { 7044 return normalizeText(node ? node.textContent : ""); 7045 } 7046 7047 function safeFocus(element) { 7048 if ( 7049 window.ENISA && 7050 window.ENISA.utils && 7051 typeof window.ENISA.utils.safeFocus === "function" 7052 ) { 7053 return window.ENISA.utils.safeFocus(element); 7054 } 7055 7056 if (!element || typeof element.focus !== "function") return false; 7057 7058 try { 7059 element.focus({ preventScroll: true }); 7060 } catch (error) { 7061 element.focus(); 7062 } 7063 7064 return true; 7065 } 7066 7067 function parseYear(value) { 7068 var text = normalizeText(value); 7069 var match = text.match(/\b(19|20)\d{2}\b/); 7070 7071 return match ? match[0] : ""; 7072 } 7073 7074 function normalizeMonthName(value) { 7075 return normalizeText(value) 7076 .toLowerCase() 7077 .replace(/[áà äâ]/g, "a") 7078 .replace(/[éèëê]/g, "e") 7079 .replace(/[Ãìïî]/g, "i") 7080 .replace(/[óòöô]/g, "o") 7081 .replace(/[úùüû]/g, "u"); 7082 } 7083 7084 function parseSpanishDate(value) { 7085 var text = normalizeText(value).toLowerCase(); 7086 7087 var fullDateMatch = text.match( 7088 /\b(\d{1,2})\s+(?:de\s+)?([a-záéÃóúüñ]+)\s+de\s+((?:19|20)\d{2})\b/i 7089 ); 7090 7091 if (fullDateMatch) { 7092 var day = parseInt(fullDateMatch[1], 10); 7093 var month = MONTHS_ES[normalizeMonthName(fullDateMatch[2])]; 7094 var year = parseInt(fullDateMatch[3], 10); 7095 7096 if (!isNaN(day) && !isNaN(year) && typeof month === "number") { 7097 return new Date(year, month, day).getTime(); 7098 } 7099 } 7100 7101 var numericDateMatch = text.match(/\b(\d{1,2})[/-](\d{1,2})[/-]((?:19|20)\d{2})\b/); 7102 7103 if (numericDateMatch) { 7104 var numericDay = parseInt(numericDateMatch[1], 10); 7105 var numericMonth = parseInt(numericDateMatch[2], 10) - 1; 7106 var numericYear = parseInt(numericDateMatch[3], 10); 7107 7108 if (!isNaN(numericDay) && !isNaN(numericMonth) && !isNaN(numericYear)) { 7109 return new Date(numericYear, numericMonth, numericDay).getTime(); 7110 } 7111 } 7112 7113 var yearOnly = parseYear(text); 7114 7115 if (yearOnly) { 7116 return new Date(parseInt(yearOnly, 10), 0, 1).getTime(); 7117 } 7118 7119 return 0; 7120 } 7121 7122 function getItemYear(item) { 7123 if (!item) return ""; 7124 7125 var explicitYear = normalizeText(item.getAttribute("data-year")); 7126 if (explicitYear) return explicitYear; 7127 7128 var dateAttr = 7129 normalizeText(item.getAttribute("data-date")) || 7130 normalizeText(item.getAttribute("data-transparency-date")); 7131 7132 if (dateAttr) { 7133 var yearFromDateAttr = parseYear(dateAttr); 7134 if (yearFromDateAttr) return yearFromDateAttr; 7135 } 7136 7137 return parseYear(getTextContent(item)); 7138 } 7139 7140 function getSortTimestamp(item) { 7141 if (!item) return 0; 7142 7143 var explicitDate = 7144 normalizeText(item.getAttribute("data-date")) || 7145 normalizeText(item.getAttribute("data-transparency-date")); 7146 7147 if (explicitDate) { 7148 var explicitTimestamp = parseSpanishDate(explicitDate); 7149 if (explicitTimestamp) return explicitTimestamp; 7150 } 7151 7152 return parseSpanishDate(getTextContent(item)); 7153 } 7154 7155 function getItemOrder(item) { 7156 if (!item) return Number.MAX_SAFE_INTEGER; 7157 7158 var rawOrder = 7159 normalizeText(item.getAttribute("data-order")) || 7160 normalizeText(item.getAttribute("data-transparency-order")); 7161 7162 var order = parseInt(rawOrder, 10); 7163 7164 return isNaN(order) ? Number.MAX_SAFE_INTEGER : order; 7165 } 7166 7167 function getColorSequenceIndex(index) { 7168 var COLOR_COUNT = 7; 7169 var safeIndex = parseInt(index, 10); 7170 7171 if (isNaN(safeIndex) || safeIndex < 1) { 7172 safeIndex = 1; 7173 } 7174 7175 return ((safeIndex - 1) % COLOR_COUNT) + 1; 7176 } 7177 7178 function collectYears(items) { 7179 var seen = {}; 7180 var years = []; 7181
7182 items.forEach(function (item) { 7183 var year = getItemYear(item); 7184 7185 if (!year || seen[year]) return; 7186 7187 seen[year] = true; 7188 years.push(year); 7189 }); 7190 7191 years.sort(function (a, b) { 7192 return parseInt(b, 10) - parseInt(a, 10); 7193 }); 7194 7195 return years; 7196 } 7197 7198 function syncNativeSelectOptions(nativeSelect, years, placeholder) { 7199 if (!nativeSelect) return; 7200 7201 var currentValue = normalizeText(nativeSelect.value); 7202 var safePlaceholder = placeholder || "Todos"; 7203 7204 nativeSelect.innerHTML = ""; 7205 7206 var allOption = document.createElement("option"); 7207 allOption.value = ""; 7208 allOption.textContent = safePlaceholder; 7209 nativeSelect.appendChild(allOption); 7210 7211 years.forEach(function (year) { 7212 var option = document.createElement("option"); 7213 option.value = year; 7214 option.textContent = year; 7215 nativeSelect.appendChild(option); 7216 }); 7217 7218 nativeSelect.value = years.indexOf(currentValue) !== -1 ? currentValue : ""; 7219 } 7220 7221 function syncCustomSelect(selectRoot) { 7222 if (!selectRoot) return; 7223 7224 var nativeSelect = selectRoot.querySelector("select"); 7225 var label = selectRoot.querySelector(".enisa-select-control__label"); 7226 var menu = selectRoot.querySelector(".enisa-select-control__menu"); 7227 7228 if (!nativeSelect || !label || !menu) return; 7229 7230 var selectedOption = nativeSelect.options[nativeSelect.selectedIndex]; 7231 label.textContent = selectedOption ? selectedOption.textContent : ""; 7232 7233 var currentValue = normalizeText(nativeSelect.value); 7234 var hasValue = currentValue !== ""; 7235 var trigger = selectRoot.querySelector(".enisa-select-control__trigger"); 7236 7237 selectRoot.classList.toggle("has-value", hasValue); 7238 7239 if (trigger) { 7240 trigger.classList.toggle("has-value", hasValue); 7241 } 7242 7243 [].forEach.call(menu.querySelectorAll(".enisa-select-control__option"), function (item) { 7244 var itemValue = normalizeText(item.getAttribute("data-value")); 7245 var isSelected = itemValue === currentValue; 7246 7247 item.classList.toggle("is-selected", isSelected); 7248 item.setAttribute("aria-selected", isSelected ? "true" : "false"); 7249 }); 7250 } 7251 7252 function initCustomSelect(selectRoot) { 7253 if (!selectRoot || selectRoot.getAttribute(INIT_SELECT_ATTR) === "true") return; 7254 7255 var nativeSelect = selectRoot.querySelector("select"); 7256 var trigger = selectRoot.querySelector(".enisa-select-control__trigger"); 7257 var label = selectRoot.querySelector(".enisa-select-control__label"); 7258 var menu = selectRoot.querySelector(".enisa-select-control__menu"); 7259 7260 if (!nativeSelect || !trigger || !label || !menu) return; 7261 7262 selectRoot.setAttribute(INIT_SELECT_ATTR, "true"); 7263 nativeSelect.setAttribute("tabindex", "-1"); 7264 nativeSelect.setAttribute("aria-hidden", "true"); 7265 7266 if (!label.id) label.id = (nativeSelect.id || "enisa-select") + "-value"; 7267 var fieldLabel = nativeSelect.id ? document.querySelector("label[for='" + nativeSelect.id.replace(/'/g, "\\'") + "']") : null; 7268 if (fieldLabel && !fieldLabel.id) fieldLabel.id = nativeSelect.id + "-label"; 7269 7270 trigger.setAttribute("aria-labelledby", (fieldLabel ? fieldLabel.id + " " : "") + label.id); 7271 trigger.setAttribute("aria-haspopup", "listbox"); 7272 trigger.setAttribute("aria-expanded", "false"); 7273 trigger.setAttribute("aria-controls", menu.id || (nativeSelect.id + "-listbox")); 7274 if (!menu.id) menu.id = nativeSelect.id + "-listbox"; 7275 menu.setAttribute("role", "listbox"); 7276 menu.setAttribute("aria-labelledby", (fieldLabel ? fieldLabel.id + " " : "") + label.id); 7277 menu.hidden = true; 7278 7279 function closeMenu() { 7280 selectRoot.classList.remove("is-open"); 7281 trigger.setAttribute("aria-expanded", "false"); 7282 menu.hidden = true; 7283 } 7284 7285 function openMenu() { 7286 selectRoot.classList.add("is-open"); 7287 trigger.setAttribute("aria-expanded", "true"); 7288 menu.hidden = false; 7289 } 7290 7291 function getOptions() { 7292 return [].slice.call(menu.querySelectorAll(".enisa-select-control__option")); 7293 } 7294 7295 function focusOption(index) { 7296 var options = getOptions(); 7297 if (!options.length) return; 7298 7299 var safeIndex = Math.max(0, Math.min(index, options.length - 1)); 7300 safeFocus(options[safeIndex]); 7301 } 7302 7303 function getSelectedIndex() { 7304 var currentValue = normalizeText(nativeSelect.value); 7305 var options = getOptions(); 7306 7307 for (var i = 0; i < options.length; i += 1) { 7308 var itemValue = normalizeText(options[i].getAttribute("data-value")); 7309 if (itemValue === currentValue) return i; 7310 } 7311 7312 return 0; 7313 } 7314 7315 function renderOptions() { 7316 menu.innerHTML = ""; 7317 7318 [].forEach.call(nativeSelect.options, function (opt) { 7319 var optionValue = normalizeText(opt.value); 7320 7321 var item = document.createElement("div"); 7322 item.className = 7323 "enisa-select-control__option" + 7324 (optionValue === "" ? " enisa-select-control__option--reset" : ""); 7325 item.setAttribute("role", "option"); 7326 item.setAttribute("tabindex", "-1"); 7327 item.setAttribute("data-value", optionValue); 7328 item.textContent = opt.textContent; 7329 7330 item.addEventListener("click", function () { 7331 nativeSelect.value = optionValue; 7332 nativeSelect.dispatchEvent(new Event("change", { bubbles: true })); 7333 syncCustomSelect(selectRoot); 7334 closeMenu(); 7335 safeFocus(trigger); 7336 }); 7337 7338 item.addEventListener("keydown", function (event) { 7339 var options = getOptions(); 7340 var index = options.indexOf(item); 7341 7342 if (event.key === "ArrowDown") { 7343 event.preventDefault(); 7344 focusOption(index + 1); 7345 return; 7346 } 7347 7348 if (event.key === "ArrowUp") { 7349 event.preventDefault(); 7350 focusOption(index - 1); 7351 return; 7352 } 7353 7354 if (event.key === "Home") { 7355 event.preventDefault(); 7356 focusOption(0); 7357 return; 7358 } 7359 7360 if (event.key === "End") { 7361 event.preventDefault(); 7362 focusOption(options.length - 1); 7363 return; 7364 } 7365 7366 if (event.key === "Escape") { 7367 event.preventDefault(); 7368 closeMenu(); 7369 safeFocus(trigger); 7370 return; 7371 } 7372 7373 if (event.key === "Enter" || event.key === " ") { 7374 event.preventDefault(); 7375 item.click(); 7376 } 7377 }); 7378 7379 menu.appendChild(item); 7380 }); 7381 7382 syncCustomSelect(selectRoot); 7383 } 7384 7385 trigger.addEventListener("click", function () { 7386 if (selectRoot.classList.contains("is-open")) { 7387 closeMenu(); 7388 } else { 7389 openMenu(); 7390 } 7391 }); 7392 7393 trigger.addEventListener("keydown", function (event) { 7394 if (event.key === "ArrowDown" || event.key === "ArrowUp") { 7395 event.preventDefault(); 7396 openMenu(); 7397 focusOption(getSelectedIndex()); 7398 return; 7399 } 7400 7401 if (event.key === "Enter" || event.key === " ") { 7402 event.preventDefault(); 7403 7404 if (selectRoot.classList.contains("is-open")) { 7405 closeMenu(); 7406 } else { 7407 openMenu(); 7408 focusOption(getSelectedIndex()); 7409 } 7410 7411 return; 7412 } 7413 7414 if (event.key === "Escape") { 7415 closeMenu(); 7416 } 7417 }); 7418 7419 nativeSelect.addEventListener("change", function () { 7420 syncCustomSelect(selectRoot); 7421 }); 7422 7423 document.addEventListener("click", function (event) { 7424 if (!selectRoot.contains(event.target)) { 7425 closeMenu(); 7426 } 7427 }); 7428 7429 renderOptions(); 7430 selectRoot.enisaRenderOptions = renderOptions; 7431 } 7432 7433 function rerenderCustomSelect(selectRoot) { 7434 if (!selectRoot) return; 7435 7436 if (typeof selectRoot.enisaRenderOptions === "function") { 7437 selectRoot.enisaRenderOptions(); 7438 return; 7439 } 7440 7441 syncCustomSelect(selectRoot); 7442 } 7443
7444 function sortItemsByDateDesc(items) { 7445 var indexed = items.map(function (item, index) { 7446 return { 7447 item: item, 7448 index: index, 7449 timestamp: getSortTimestamp(item), 7450 year: parseInt(getItemYear(item) || "0", 10), 7451 order: getItemOrder(item) 7452 }; 7453 }); 7454 7455 indexed.sort(function (a, b) { 7456 if (b.year !== a.year) return b.year - a.year; 7457 if (b.timestamp !== a.timestamp) return b.timestamp - a.timestamp; 7458 if (a.order !== b.order) return a.order - b.order; 7459 return a.index - b.index; 7460 }); 7461 7462 return indexed.map(function (entry) { 7463 return entry.item; 7464 }); 7465 } 7466 7467 function setRowVisibility(row, isVisible) { 7468 row.hidden = !isVisible; 7469 } 7470 7471 function ensureStatus(scope, key) { 7472 var root = scope || document; 7473 var attr = "data-enisa-status-" + key; 7474 var status = root.querySelector ? root.querySelector("[" + attr + "]") : null; 7475 7476 if (status) return status; 7477 7478 status = document.createElement("p"); 7479 status.className = "enisa-sr-only"; 7480 status.setAttribute(attr, ""); 7481 status.setAttribute("role", "status"); 7482 status.setAttribute("aria-live", "polite"); 7483 status.setAttribute("aria-atomic", "true"); 7484 7485 if (root.appendChild && root.nodeType === 1) { 7486 root.appendChild(status); 7487 } else { 7488 document.body.appendChild(status); 7489 } 7490 7491 return status; 7492 } 7493 7494 function announce(status, message) { 7495 if (!status) return; 7496 status.textContent = ""; 7497 window.setTimeout(function () { 7498 status.textContent = message || ""; 7499 }, 20); 7500 } 7501 7502 function getTransparencyScope(root) { 7503 if (!root) return document; 7504 7505 return root.closest(TRANSPARENCY_SCOPE_SELECTOR) || root.parentElement || document; 7506 } 7507 7508 function syncTableCards(scope) { 7509 if ( 7510 window.ENISA && 7511 window.ENISA.tables && 7512 typeof window.ENISA.tables.sync === "function" 7513 ) { 7514 window.ENISA.tables.sync(scope || document); 7515 } 7516 } 7517 7518 function normalizeVisibleDocumentLabels(scope) { 7519 if (!scope) return; 7520 7521 var labels = scope.querySelectorAll( 7522 ".enisa-recursos-detalle__download-label, " + 7523 ".enisa-transparencia-documents__cell:first-child, " + 7524 ".enisa-transparencia-documents__mobile-title, " + 7525 ".enisa-transparencia-documents-card__title, " + 7526 ".enisa-transparencia-documents td:first-child, " + 7527 ".enisa-table-card__item--primary .enisa-table-card__value" 7528 ); 7529 7530 [].forEach.call(labels, function (label) { 7531 if (!label || !label.textContent) return; 7532 label.textContent = normalizeFilenameLabel(label.textContent); 7533 }); 7534 } 7535 7536 function initTransparencyTable(tableRoot) { 7537 if (!tableRoot || tableRoot.getAttribute(INIT_TABLE_ATTR) === "true") return; 7538 7539 var scope = getTransparencyScope(tableRoot); 7540 var rows = [].slice.call(tableRoot.querySelectorAll("[data-transparency-row]")); 7541 var yearSelect = 7542 scope.querySelector("[" + YEAR_FILTER_ATTR + "]") || 7543 tableRoot.querySelector("[" + YEAR_FILTER_ATTR + "]"); 7544 var emptyState = 7545 scope.querySelector("[data-transparency-empty]") || 7546 tableRoot.querySelector("[data-transparency-empty]"); 7547 var status = ensureStatus(scope, "transparency-table"); 7548 7549 if (!rows.length) return; 7550 7551 tableRoot.setAttribute(INIT_TABLE_ATTR, "true"); 7552 7553 rows.forEach(function (row) { 7554 var year = getItemYear(row); 7555 7556 if (year) { 7557 row.setAttribute("data-year", year); 7558 } 7559 }); 7560 7561 var sortedRows = sortItemsByDateDesc(rows); 7562 7563 sortedRows.forEach(function (row) { 7564 if (row.parentNode) { 7565 row.parentNode.appendChild(row); 7566 } 7567 }); 7568 7569 if (yearSelect) { 7570 var years = collectYears(sortedRows); 7571 var selectRoot = yearSelect.closest(SELECT_SELECTOR); 7572 7573 initCustomSelect(selectRoot); 7574 7575 if (years.length > 1) { 7576 syncNativeSelectOptions(yearSelect, years, "Todos"); 7577 rerenderCustomSelect(selectRoot); 7578 } else if (selectRoot) { 7579 var filterRoot = selectRoot.closest(".enisa-transparencia-documents__filter"); 7580 7581 if (filterRoot) { 7582 filterRoot.setAttribute("hidden", ""); 7583 } 7584 } 7585 } 7586 7587 function applyYearFilter() { 7588 var selected = yearSelect ? normalizeText(yearSelect.value) : ""; 7589 var visibleCount = 0; 7590 var activeRow = document.activeElement && document.activeElement.closest 7591 ? document.activeElement.closest("[data-transparency-row]") 7592 : null; 7593 var activeRowWillHide = false;
7594 7595 sortedRows.forEach(function (row) { 7596 var rowYear = getItemYear(row); 7597 var isVisible = selected === "" || selected === rowYear; 7598 7599 if (row === activeRow && !isVisible) { 7600 activeRowWillHide = true; 7601 } 7602 7603 setRowVisibility(row, isVisible); 7604 7605 if (isVisible) { 7606 visibleCount += 1; 7607 } 7608 }); 7609 7610 if (activeRowWillHide && yearSelect) { 7611 var focusTarget = (yearSelect.closest(SELECT_SELECTOR) || scope) 7612 .querySelector(".enisa-select-control__trigger") || yearSelect; 7613 7614 if ( 7615 focusTarget && 7616 window.ENISA && 7617 window.ENISA.utils && 7618 typeof window.ENISA.utils.focusWithoutScroll === "function" 7619 ) { 7620 window.ENISA.utils.focusWithoutScroll(focusTarget); 7621 } else if (focusTarget && typeof focusTarget.focus === "function") { 7622 safeFocus(focusTarget); 7623 } 7624 } 7625 7626 if (emptyState) { 7627 emptyState.hidden = visibleCount > 0; 7628 } 7629 7630 syncTableCards(scope); 7631 announce(status, visibleCount + (visibleCount === 1 ? " documento visible." : " documentos visibles.")); 7632 } 7633 7634 if (yearSelect) { 7635 yearSelect.addEventListener("change", applyYearFilter); 7636 } 7637 7638 normalizeVisibleDocumentLabels(scope); 7639 applyYearFilter(); 7640 } 7641 7642 function createDetailYearPills(detailRoot, years) { 7643 if (!detailRoot || !years || years.length <= 1) return null; 7644 7645 var list = detailRoot.querySelector(RESOURCE_LIST_SELECTOR); 7646 if (!list) return null; 7647 7648 var existing = detailRoot.querySelector("[data-transparency-detail-year-pills]"); 7649 if (existing) return existing; 7650 7651 var heading = 7652 list.querySelector(".enisa-recursos-detalle__item--heading-with-filter") || 7653 list.querySelector(".enisa-recursos-detalle__item--heading"); 7654 7655 var wrapper = document.createElement("div"); 7656 wrapper.className = "enisa-recursos-detalle__year-pills enisa-news-filters"; 7657 wrapper.setAttribute("data-transparency-detail-year-pills", ""); 7658 wrapper.setAttribute("role", "group"); 7659 wrapper.setAttribute("aria-label", "Filtrar documentos por año"); 7660 7661 function createButton(label, value, index, active) { 7662 var button = document.createElement("button"); 7663 7664 button.type = "button"; 7665 button.className = "enisa-news-filters__pill" + (active ? " is-active" : ""); 7666 button.setAttribute("data-transparency-detail-year-pill", value); 7667 button.setAttribute("data-color-index", String(getColorSequenceIndex(index))); 7668 button.setAttribute("aria-pressed", active ? "true" : "false"); 7669 button.textContent = label; 7670 7671 return button; 7672 } 7673 7674 wrapper.appendChild(createButton("Todos", "", 1, true)); 7675 7676 years.forEach(function (year, index) { 7677 wrapper.appendChild(createButton(year, year, index + 2, false)); 7678 }); 7679 7680 if (heading && heading.nextSibling) { 7681 list.insertBefore(wrapper, heading.nextSibling); 7682 } else if (heading) { 7683 list.appendChild(wrapper); 7684 } else { 7685 list.insertBefore(wrapper, list.firstChild); 7686 } 7687 7688 return wrapper; 7689 } 7690 7691 function setPillsState(pillsRoot, selectedYear) { 7692 if (!pillsRoot) return; 7693 7694 var selected = normalizeText(selectedYear); 7695 7696 [].forEach.call( 7697 pillsRoot.querySelectorAll("[data-transparency-detail-year-pill]"), 7698 function (button) { 7699 var value = normalizeText(button.getAttribute("data-transparency-detail-year-pill")); 7700 var isActive = value === selected; 7701 7702 button.classList.toggle("is-active", isActive); 7703 button.setAttribute("aria-pressed", isActive ? "true" : "false"); 7704 } 7705 ); 7706 } 7707 7708 function getDetailResourceItems(detailRoot) { 7709 if (!detailRoot) return []; 7710 7711 return [].slice 7712 .call(detailRoot.querySelectorAll(RESOURCE_ITEM_SELECTOR)) 7713 .filter(function (item) { 7714 return ( 7715 !item.matches("[data-transparency-detail-filter], .enisa-recursos-detalle__empty") && 7716 item.getAttribute("data-transparency-detail-filterable") === "true" 7717 ); 7718 }); 7719 } 7720 7721 function createDetailPagination(detailRoot) { 7722 if (!detailRoot) return null; 7723 7724 var existing = detailRoot.querySelector("[data-transparency-detail-pagination]"); 7725 if (existing) return existing; 7726 7727 var list = detailRoot.querySelector(RESOURCE_LIST_SELECTOR); 7728 if (!list) return null; 7729 7730 var pagination = document.createElement("nav"); 7731 pagination.className = "enisa-recursos-detalle__pagination enisa-news-filters"; 7732 pagination.setAttribute("data-transparency-detail-pagination", ""); 7733 pagination.setAttribute("aria-label", "Paginación de documentos"); 7734 7735 list.parentNode.insertBefore(pagination, list.nextSibling); 7736 7737 detailRoot.enisaTransparencyPagination = { 7738 root: pagination, 7739 currentPage: 1, 7740 pageSize: DETAIL_PAGE_SIZE, 7741 focusPageAfterRender: false 7742 }; 7743 7744 pagination.addEventListener("click", function (event) { 7745 var button = event.target.closest("[data-transparency-detail-page]"); 7746 if (!button || !pagination.contains(button)) return; 7747 7748 var nextPage = parseInt(button.getAttribute("data-transparency-detail-page"), 10); 7749 if (isNaN(nextPage)) return; 7750 7751 detailRoot.enisaTransparencyPagination.currentPage = nextPage; 7752 detailRoot.enisaTransparencyPagination.focusPageAfterRender = true; 7753 updateDetailPagination(detailRoot); 7754 }); 7755 7756 return pagination; 7757 } 7758 7759 function setPaginationButtonState(button, isCurrent) { 7760 if (!button) return; 7761 7762 button.classList.toggle("is-active", isCurrent); 7763 button.setAttribute("aria-current", isCurrent ? "page" : "false"); 7764 button.setAttribute("aria-label", isCurrent ? "Página " + button.textContent + ", página actual" : "Ir a la página " + button.textContent); 7765 } 7766 7767 function updateSectionHeadingsVisibility(detailRoot) { 7768 if (!detailRoot) return; 7769 7770 var headings = [].slice.call( 7771 detailRoot.querySelectorAll(".enisa-recursos-detalle__item--heading") 7772 ); 7773
7774 headings.forEach(function (heading) { 7775 var node = heading.nextElementSibling; 7776 var hasVisibleItem = false; 7777 7778 while (node && !node.classList.contains("enisa-recursos-detalle__item--heading")) { 7779 if ( 7780 node.classList.contains("enisa-recursos-detalle__item") && 7781 !node.matches(".enisa-recursos-detalle__empty") && 7782 node.hidden === false 7783 ) { 7784 hasVisibleItem = true; 7785 break; 7786 } 7787 7788 node = node.nextElementSibling; 7789 } 7790 7791 heading.hidden = !hasVisibleItem; 7792 }); 7793 } 7794 7795 function renderDetailPaginationControls(controller, pages) { 7796 if (!controller || !controller.root) return; 7797 7798 var root = controller.root; 7799 root.innerHTML = ""; 7800 7801 if (pages <= 1) { 7802 root.hidden = true; 7803 controller.focusPageAfterRender = false; 7804 return; 7805 } 7806 7807 root.hidden = false; 7808 7809 for (var i = 1; i <= pages; i += 1) { 7810 var button = document.createElement("button"); 7811 button.type = "button"; 7812 button.className = "enisa-news-filters__pill"; 7813 button.setAttribute("data-transparency-detail-page", String(i)); 7814 button.setAttribute("data-color-index", String(getColorSequenceIndex(i))); 7815 button.textContent = String(i); 7816 setPaginationButtonState(button, i === controller.currentPage); 7817 root.appendChild(button); 7818 } 7819 7820 if (controller.focusPageAfterRender) { 7821 var focusTarget = root.querySelector( 7822 "[data-transparency-detail-page='" + controller.currentPage + "']" 7823 ); 7824 7825 controller.focusPageAfterRender = false;
7826 7827 if ( 7828 focusTarget && 7829 window.ENISA && 7830 window.ENISA.utils && 7831 typeof window.ENISA.utils.focusWithoutScroll === "function" 7832 ) { 7833 window.ENISA.utils.focusWithoutScroll(focusTarget); 7834 } else if (focusTarget && typeof focusTarget.focus === "function") { 7835 safeFocus(focusTarget); 7836 } 7837 } 7838 } 7839 7840 function updateDetailPagination(detailRoot) { 7841 if (!detailRoot || !detailRoot.enisaTransparencyPagination) return; 7842 7843 var controller = detailRoot.enisaTransparencyPagination; 7844 var items = getDetailResourceItems(detailRoot); 7845 var availableItems = items.filter(function (item) { 7846 return item.getAttribute("data-transparency-filter-hidden") !== "true"; 7847 }); 7848 7849 var pages = Math.max(1, Math.ceil(availableItems.length / controller.pageSize)); 7850 7851 if (controller.currentPage > pages) controller.currentPage = pages; 7852 if (controller.currentPage < 1) controller.currentPage = 1; 7853 7854 var start = (controller.currentPage - 1) * controller.pageSize; 7855 var end = start + controller.pageSize; 7856 var activeItem = document.activeElement && document.activeElement.closest 7857 ? document.activeElement.closest(RESOURCE_ITEM_SELECTOR) 7858 : null; 7859 7860 if ( 7861 activeItem && 7862 items.indexOf(activeItem) > -1 && 7863 availableItems.indexOf(activeItem) !== -1 && 7864 (availableItems.indexOf(activeItem) < start || availableItems.indexOf(activeItem) >= end) 7865 ) { 7866 controller.focusPageAfterRender = true; 7867 } 7868 7869 items.forEach(function (item) { 7870 var isFilteredHidden = item.getAttribute("data-transparency-filter-hidden") === "true"; 7871 var availableIndex = availableItems.indexOf(item); 7872 var isOnPage = availableIndex >= start && availableIndex < end; 7873 7874 item.hidden = isFilteredHidden || !isOnPage; 7875 }); 7876 7877 renderDetailPaginationControls(controller, pages); 7878 updateSectionHeadingsVisibility(detailRoot); 7879 } 7880 7881 function initDetailPagination(detailRoot) { 7882 if (!detailRoot) return; 7883 7884 var items = getDetailResourceItems(detailRoot); 7885 if (!items.length) return; 7886 7887 items.forEach(function (item) { 7888 if (!item.hasAttribute("data-transparency-filter-hidden")) { 7889 item.setAttribute("data-transparency-filter-hidden", "false"); 7890 } 7891 }); 7892 7893 createDetailPagination(detailRoot); 7894 updateDetailPagination(detailRoot); 7895 } 7896 7897 function initTransparencyResourceDetail(detailRoot) { 7898 if (!detailRoot || detailRoot.getAttribute(INIT_DETAIL_ATTR) === "true") return; 7899 7900 var list = detailRoot.querySelector(RESOURCE_LIST_SELECTOR); 7901 if (!list) return; 7902 7903 var allItems = [].slice 7904 .call(list.querySelectorAll(RESOURCE_ITEM_SELECTOR)) 7905 .filter(function (item) { 7906 return !item.matches("[data-transparency-detail-filter], .enisa-recursos-detalle__empty"); 7907 }); 7908 7909 if (!allItems.length) return; 7910 7911 detailRoot.setAttribute(INIT_DETAIL_ATTR, "true"); 7912 7913 var items = allItems.filter(function (item) { 7914 return item.getAttribute("data-transparency-detail-filterable") === "true"; 7915 }); 7916 7917 if (!items.length) { 7918 initDetailPagination(detailRoot); 7919 normalizeVisibleDocumentLabels(detailRoot); 7920 return; 7921 } 7922 7923 items.forEach(function (item) { 7924 var year = getItemYear(item); 7925 7926 if (year) { 7927 item.setAttribute("data-year", year); 7928 } 7929 }); 7930 7931 var sortedItems = sortItemsByDateDesc(items); 7932 7933 sortedItems.forEach(function (item) { 7934 list.appendChild(item); 7935 }); 7936 7937 initDetailPagination(detailRoot); 7938 7939 var years = collectYears(sortedItems); 7940 var pillsRoot = createDetailYearPills(detailRoot, years); 7941 var currentYear = ""; 7942 var status = ensureStatus(detailRoot, "transparency-detail"); 7943 7944 function getEmptyState() { 7945 var empty = detailRoot.querySelector("[data-transparency-detail-empty]"); 7946 7947 if (empty) return empty; 7948 7949 empty = document.createElement("div"); 7950 empty.className = "enisa-recursos-detalle__empty"; 7951 empty.setAttribute("data-transparency-detail-empty", ""); 7952 empty.hidden = true; 7953 7954 var emptyText = document.createElement("p"); 7955 emptyText.className = "enisa-recursos-detalle__empty-text"; 7956 emptyText.textContent = "No hay documentos para el año seleccionado."; 7957 7958 empty.appendChild(emptyText); 7959 list.appendChild(empty); 7960 7961 return empty; 7962 } 7963 7964 function applyYearFilter(selectedYear) {
7965 currentYear = normalizeText(selectedYear); 7966 var visibleCount = 0; 7967 7968 sortedItems.forEach(function (item) { 7969 var itemYear = getItemYear(item); 7970 var isVisible = currentYear === "" || currentYear === itemYear; 7971 7972 item.setAttribute("data-transparency-filter-hidden", isVisible ? "false" : "true"); 7973 item.hidden = !isVisible; 7974 7975 if (isVisible) { 7976 visibleCount += 1; 7977 } 7978 }); 7979 7980 setPillsState(pillsRoot, currentYear); 7981 7982 var empty = getEmptyState(); 7983 empty.hidden = visibleCount > 0; 7984 7985 updateDetailPagination(detailRoot); 7986 announce(status, visibleCount + (visibleCount === 1 ? " documento visible." : " documentos visibles.")); 7987 } 7988 7989 if (pillsRoot) { 7990 pillsRoot.addEventListener("click", function (event) { 7991 var button = event.target.closest("[data-transparency-detail-year-pill]"); 7992 if (!button || !pillsRoot.contains(button)) return; 7993 7994 applyYearFilter(button.getAttribute("data-transparency-detail-year-pill")); 7995 }); 7996 7997 pillsRoot.addEventListener("keydown", function (event) { 7998 var currentButton = event.target.closest("[data-transparency-detail-year-pill]"); 7999 if (!currentButton) return; 8000 8001 var buttons = [].slice.call( 8002 pillsRoot.querySelectorAll("[data-transparency-detail-year-pill]") 8003 ); 8004 8005 var currentIndex = buttons.indexOf(currentButton); 8006 var nextIndex = currentIndex; 8007 8008 if (event.key === "ArrowRight" || event.key === "ArrowDown") { 8009 nextIndex = (currentIndex + 1) % buttons.length; 8010 } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") { 8011 nextIndex = (currentIndex - 1 + buttons.length) % buttons.length; 8012 } else if (event.key === "Home") { 8013 nextIndex = 0; 8014 } else if (event.key === "End") { 8015 nextIndex = buttons.length - 1; 8016 } else { 8017 return; 8018 } 8019 8020 event.preventDefault(); 8021 8022 if (buttons[nextIndex]) { 8023 safeFocus(buttons[nextIndex]); 8024 } 8025 }); 8026 } 8027 8028 normalizeVisibleDocumentLabels(detailRoot); 8029 applyYearFilter(currentYear); 8030 } 8031 8032 function markLinkAsNewTab(link) { 8033 if (!link) return; 8034 8035 link.setAttribute("target", "_blank"); 8036 link.setAttribute("rel", "noopener noreferrer"); 8037 8038 var currentLabel = normalizeText(link.getAttribute("aria-label")); 8039 var suffix = "se abre en una nueva ventana"; 8040 8041 if (currentLabel) { 8042 if (currentLabel.toLowerCase().indexOf(suffix) === -1) { 8043 link.setAttribute("aria-label", currentLabel + " (" + suffix + ")"); 8044 } 8045 8046 return; 8047 } 8048 8049 var text = getTextContent(link); 8050 8051 if (text) { 8052 link.setAttribute("aria-label", text + " (" + suffix + ")"); 8053 } 8054 } 8055 8056 function initDocumentLinks(scope) { 8057 var links = scope.querySelectorAll( 8058 ".enisa-transparencia-documents a[href], " + 8059 ".enisa-transparencia-table a[href], " + 8060 ".enisa-recursos-detalle--transparencia .enisa-recursos-detalle__download[href]" 8061 ); 8062 8063 [].forEach.call(links, markLinkAsNewTab); 8064 } 8065 8066 function formatBytes(bytes) { 8067 if (!bytes || isNaN(bytes) || bytes <= 0) return "-"; 8068 8069 var units = ["B", "KB", "MB", "GB"]; 8070 var size = bytes; 8071 var unitIndex = 0; 8072 8073 while (size >= 1024 && unitIndex < units.length - 1) { 8074 size = size / 1024; 8075 unitIndex += 1; 8076 } 8077 8078 var decimals = unitIndex === 0 ? 0 : size >= 10 ? 1 : 2; 8079 8080 return size.toFixed(decimals).replace(".", ",") + " " + units[unitIndex]; 8081 } 8082 8083 function canResolveDocumentSize(url) { 8084 if (!url) return false; 8085 8086 var normalizedUrl = url.toString().trim().toLowerCase(); 8087 8088 if (!normalizedUrl) return false; 8089 if (normalizedUrl.indexOf("mailto:") === 0) return false; 8090 if (normalizedUrl.indexOf("tel:") === 0) return false; 8091 8092 return ( 8093 normalizedUrl.indexOf("/") === 0 || 8094 normalizedUrl.indexOf(window.location.origin.toLowerCase()) === 0 8095 ); 8096 } 8097 8098 function resolveDocumentSize(sizeNode) { 8099 if (!sizeNode || sizeNode.getAttribute("data-document-size-init") === "true") return; 8100 8101 var url = normalizeText(sizeNode.getAttribute("data-document-size-url")); 8102 8103 if (!canResolveDocumentSize(url)) return; 8104 8105 sizeNode.setAttribute("data-document-size-init", "true"); 8106 8107 fetch(url, { 8108 method: "HEAD", 8109 credentials: "same-origin" 8110 }) 8111 .then(function (response) { 8112 if (!response || !response.ok) return null; 8113 8114 var length = response.headers.get("content-length"); 8115 var parsedLength = length ? parseInt(length, 10) : 0; 8116 8117 return parsedLength > 0 ? parsedLength : null; 8118 }) 8119 .then(function (bytes) { 8120 if (!bytes) return; 8121 8122 sizeNode.textContent = formatBytes(bytes); 8123 8124 syncTableCards(document); 8125 }) 8126 .catch(function () { 8127 sizeNode.textContent = "-"; 8128 }); 8129 } 8130 8131 function initDocumentSizes(scope) { 8132 var sizeNodes = scope.querySelectorAll("[data-document-size]"); 8133 8134 [].forEach.call(sizeNodes, resolveDocumentSize); 8135 } 8136
8137 function init() { 8138 var tables = document.querySelectorAll(TABLE_SELECTOR); 8139 [].forEach.call(tables, initTransparencyTable); 8140 8141 var details = document.querySelectorAll(RESOURCE_DETAIL_SELECTOR); 8142 [].forEach.call(details, initTransparencyResourceDetail); 8143 8144 normalizeVisibleDocumentLabels(document); 8145 initDocumentLinks(document); 8146 initDocumentSizes(document); 8147 } 8148 8149 if (document.readyState === "loading") { 8150 document.addEventListener("DOMContentLoaded", init); 8151 } else { 8152 init(); 8153 } 8154})(); 8155 8156/* 8157 ENISA | JS Module 8158 Modulo: 20-accordion 8159 Descripcion: Acordeones fluidos, accesibles y estables para Power Pages. 8160*/ 8161(function () { 8162 'use strict'; 8163 8164 var INIT_FLAG = 'data-accord-init'; 8165 var FAQS_INIT_FLAG = 'data-faqs-init'; 8166 var FAQ_TRIGGER_SELECTOR = '[data-faq-btn], [data-faq-trigger]'; 8167 var FIN_TRIGGER_SELECTOR = '[data-fin-accord-btn]'; 8168 var DEFAULT_DURATION = '0.42s'; 8169 var DEFAULT_DURATION_MS = 420; 8170 var prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 8171 8172 function toArray(list) { 8173 return Array.prototype.slice.call(list || []); 8174 } 8175 8176 function debounce(fn, ms) { 8177 var timer; 8178 8179 return function () { 8180 var args = arguments; 8181 var context = this; 8182 8183 clearTimeout(timer); 8184 8185 timer = setTimeout(function () { 8186 fn.apply(context, args); 8187 }, ms || 150); 8188 }; 8189 } 8190 8191 function parseDurationToMs(rawDuration) { 8192 var value = (rawDuration || '').toString().trim(); 8193 8194 if (!value) return DEFAULT_DURATION_MS; 8195 if (value.indexOf('ms') > -1) return parseFloat(value) || DEFAULT_DURATION_MS; 8196 if (value.indexOf('s') > -1) return (parseFloat(value) || 0.42) * 1000; 8197 8198 return parseFloat(value) || DEFAULT_DURATION_MS; 8199 } 8200 8201 function getPanelDuration(panel) { 8202 if (prefersReducedMotion) return '0.001s'; 8203 if (!panel || !window.getComputedStyle) return DEFAULT_DURATION; 8204 8205 var fromCss = window.getComputedStyle(panel).getPropertyValue('--enisa-accordion-duration'); 8206 8207 return (fromCss || '').trim() || DEFAULT_DURATION; 8208 } 8209 8210 function getPanelDurationMs(panel) { 8211 return parseDurationToMs(getPanelDuration(panel)); 8212 } 8213 8214 function resolvePanel(panelId, root) { 8215 if (!panelId) return null; 8216 8217 var panel = document.getElementById(panelId); 8218 8219 if (panel) return panel; 8220 if (!root || !root.querySelector) return null; 8221 8222 return root.querySelector('[id="' + panelId.replace(/"/g, '\\"') + '"]'); 8223 } 8224 8225 function getControlledPanel(btn, root) { 8226 return btn ? resolvePanel(btn.getAttribute('aria-controls'), root || document) : null; 8227 } 8228 8229 function setPanelInteractive(panel, isInteractive) { 8230 if ( 8231 window.ENISA && 8232 window.ENISA.utils && 8233 typeof window.ENISA.utils.setSubtreeTabbable === 'function' 8234 ) { 8235 window.ENISA.utils.setSubtreeTabbable(panel, isInteractive); 8236 } 8237 } 8238 8239 function safeFocus(element) { 8240 if ( 8241 window.ENISA && 8242 window.ENISA.utils && 8243 typeof window.ENISA.utils.safeFocus === 'function' 8244 ) { 8245 return window.ENISA.utils.safeFocus(element); 8246 } 8247 8248 if (!element || typeof element.focus !== 'function') return false; 8249 8250 try { 8251 element.focus({ preventScroll: true }); 8252 } catch (error) { 8253 element.focus(); 8254 } 8255 8256 return true; 8257 } 8258 8259 function getItem(btn) { 8260 return btn ? (btn.closest('[data-fin-accord], [data-faq-item]') || btn.parentNode) : null; 8261 } 8262 8263 function setVisualState(btn, panel, isOpen) { 8264 if (!btn || !panel) return; 8265 8266 var item = getItem(btn); 8267 8268 btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 8269 panel.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); 8270 panel.classList.toggle('is-open', !!isOpen); 8271 8272 if (item && item.classList) { 8273 item.classList.toggle('is-open', !!isOpen); 8274 } 8275 } 8276 8277 function clearPanelTimers(panel) { 8278 if (!panel) return; 8279 8280 if (panel._enisaEndHandler) { 8281 panel.removeEventListener('transitionend', panel._enisaEndHandler); 8282 panel._enisaEndHandler = null; 8283 } 8284 8285 if (panel._enisaSafetyTimer) { 8286 clearTimeout(panel._enisaSafetyTimer); 8287 panel._enisaSafetyTimer = null; 8288 } 8289 8290 if (panel._enisaResizeObserver) { 8291 panel._enisaResizeObserver.disconnect(); 8292 panel._enisaResizeObserver = null; 8293 } 8294 } 8295 8296 function resetPanelInlineStyles(panel) { 8297 if (!panel) return; 8298 8299 panel.style.height = ''; 8300 panel.style.overflow = ''; 8301 panel.style.opacity = ''; 8302 panel.style.transition = ''; 8303 panel.removeAttribute('data-accord-animating'); 8304 } 8305 8306 function stopPanelAnimation(panel) { 8307 if (!panel) return; 8308 8309 clearPanelTimers(panel); 8310 resetPanelInlineStyles(panel); 8311 } 8312 8313 function setupDynamicHeightSync(panel) { 8314 if (!panel || !('ResizeObserver' in window)) return; 8315 8316 var inner = panel.firstElementChild || panel; 8317 8318 panel._enisaResizeObserver = new ResizeObserver(function () { 8319 if (panel.getAttribute('data-accord-animating') !== 'opening') return; 8320 8321 panel.style.height = panel.scrollHeight + 'px'; 8322 }); 8323 8324 panel._enisaResizeObserver.observe(inner); 8325 } 8326 8327 function finishOpen(panel, callback) { 8328 clearPanelTimers(panel); 8329 resetPanelInlineStyles(panel); 8330 panel.classList.add('is-open'); 8331 8332 if (typeof callback === 'function') callback(); 8333 } 8334 8335 function finishClose(panel, callback) { 8336 clearPanelTimers(panel); 8337 panel.setAttribute('hidden', ''); 8338 panel.classList.remove('is-open'); 8339 resetPanelInlineStyles(panel); 8340 8341 toArray(panel.querySelectorAll('.enisa-accordion__grid')).forEac
8341h(function (grid) { 8342 grid.scrollLeft = 0; 8343 }); 8344 8345 if (typeof callback === 'function') callback(); 8346 } 8347 8348 function expandPanel(panel, callback) { 8349 if (!panel) return; 8350 8351 stopPanelAnimation(panel); 8352 8353 panel.removeAttribute('hidden'); 8354 panel.classList.add('is-open'); 8355 8356 if (prefersReducedMotion) { 8357 finishOpen(panel, callback); 8358 return; 8359 } 8360 8361 var duration = getPanelDuration(panel); 8362 var durationMs = getPanelDurationMs(panel); 8363 var targetHeight = panel.scrollHeight; 8364 8365 if (!targetHeight) { 8366 finishOpen(panel, callback); 8367 return; 8368 } 8369 8370 panel.setAttribute('data-accord-animating', 'opening'); 8371 panel.style.overflow = 'hidden'; 8372 panel.style.height = '0px'; 8373 panel.style.opacity = '0'; 8374 8375 setupDynamicHeightSync(panel); 8376 8377 panel.offsetHeight; 8378 panel.style.transition = 'height ' + duration + ' cubic-bezier(0.22, 1, 0.36, 1), opacity ' + duration + ' ease-out'; 8379 panel.style.height = targetHeight + 'px'; 8380 panel.style.opacity = '1'; 8381 8382 function onEnd(event) { 8383 if (event && event.target !== panel) return; 8384 if (event && event.propertyName !== 'height') return; 8385 8386 finishOpen(panel, callback); 8387 } 8388 8389 panel._enisaEndHandler = onEnd; 8390 panel.addEventListener('transitionend', onEnd); 8391 8392 panel._enisaSafetyTimer = setTimeout(function () { 8393 finishOpen(panel, callback); 8394 }, durationMs + 90); 8395 } 8396 8397 function collapsePanel(panel, callback) { 8398 if (!panel) return; 8399 8400 stopPanelAnimation(panel); 8401 8402 if (prefersReducedMotion || panel.hasAttribute('hidden')) { 8403 finishClose(panel, callback); 8404 return; 8405 } 8406 8407 var duration = getPanelDuration(panel); 8408 var durationMs = getPanelDurationMs(panel); 8409 var startHeight = Math.max(panel.getBoundingClientRect().height || 0, panel.scrollHeight || 0); 8410 8411 if (!startHeight) { 8412 finishClose(panel, callback); 8413 return; 8414 } 8415 8416 panel.setAttribute('data-accord-animating', 'closing'); 8417 panel.style.overflow = 'hidden'; 8418 panel.style.height = startHeight + 'px'; 8419 panel.style.opacity = '1'; 8420 8421 panel.offsetHeight; 8422 panel.style.transition = 'height ' + duration + ' cubic-bezier(0.22, 1, 0.36, 1), opacity ' + duration + ' ease-in'; 8423 panel.style.height = '0px'; 8424 panel.style.opacity = '0'; 8425 8426 function onEnd(event) { 8427 if (event && event.target !== panel) return; 8428 if (event && event.propertyName !== 'height') return; 8429 8430 finishClose(panel, callback); 8431 } 8432 8433 panel._enisaEndHandler = onEnd; 8434 panel.addEventListener('transitionend', onEnd); 8435 8436 panel._enisaSafetyTimer = setTimeout(function () { 8437 finishClose(panel, callback); 8438 }, durationMs + 90); 8439 } 8440 8441 function closeButton(btn, root, callback) { 8442 var panel = getControlledPanel(btn, root); 8443 8444 if (!panel || btn.getAttribute('aria-expanded') !== 'true') { 8445 if (typeof callback === 'function') callback(); 8446 return; 8447 } 8448 8449 if ( 8450 window.ENISA && 8451 window.ENISA.utils && 8452 typeof window.ENISA.utils.focusIfActiveInside === 'function' 8453 ) { 8454 window.ENISA.utils.focusIfActiveInside(panel, btn); 8455 } else if (panel.contains(document.activeElement) && typeof btn.focus === 'function') { 8456 safeFocus(btn); 8457 } 8458 8459 setVisualState(btn, panel, false); 8460 setPanelInteractive(panel, false); 8461 collapsePanel(panel, callback); 8462 } 8463 8464 function openButton(btn, root) { 8465 var panel = getControlledPanel(btn, root); 8466 8467 if (!panel || btn.getAttribute('aria-expanded') === 'true') return; 8468 8469 initAccordionGridCarousels(panel); 8470 setVisualState(btn, panel, true); 8471 setPanelInteractive(panel, true); 8472 expandPanel(panel); 8473 } 8474 8475 var BREAKPOINT_MOBILE = 767.98; 8476 var BREAKPOINT_TABLET = 1023.98; 8477 8478 function getCurrentLayout() { 8479 var w = window.innerWidth || document.documentElement.clientWidth || 0; 8480 8481 if (w <= BREAKPOINT_MOBILE) return 'mobile'; 8482 if (w <= BREAKPOINT_TABLET) return 'tablet'; 8483 8484 return 'desktop'; 8485 } 8486 8487 function applyBalancedLayout(container, itemSelector, halfClass, fullClass, thirdClass) { 8488 if (!container) return; 8489 8490 var items = toArray(container.querySelectorAll(itemSelector)); 8491 var layout = getCurrentLayout(); 8492 var n = items.length; 8493 var i; 8494
8495 items.forEach(function (item) { 8496 item.classList.remove(halfClass, fullClass, thirdClass); 8497 }); 8498 8499 if (!n || layout === 'mobile') return; 8500 8501 if (n === 1) { 8502 items[0].classList.add(fullClass); 8503 return; 8504 } 8505 8506 if (n === 2) { 8507 items[0].classList.add(halfClass); 8508 items[1].classList.add(halfClass); 8509 return; 8510 } 8511 8512 if (layout === 'tablet') { 8513 if (n === 3) { 8514 for (i = 0; i < n; i += 1) items[i].classList.add(thirdClass); 8515 return; 8516 } 8517 8518 if (n % 2 !== 0) { 8519 items[n - 3].classList.add(thirdClass); 8520 items[n - 2].classList.add(thirdClass); 8521 items[n - 1].classList.add(thirdClass); 8522 } 8523 8524 return; 8525 } 8526 8527 if (n % 3 === 2) { 8528 items[n - 2].classList.add(halfClass); 8529 items[n - 1].classList.add(halfClass); 8530 return; 8531 } 8532 8533 if (n % 3 === 1) { 8534 if (n === 4) { 8535 for (i = 0; i < n; i += 1) items[i].classList.add(halfClass); 8536 return; 8537 } 8538 8539 items[n - 4].classList.add(halfClass); 8540 items[n - 3].classList.add(halfClass); 8541 items[n - 2].classList.add(halfClass); 8542 items[n - 1].classList.add(halfClass); 8543 } 8544 } 8545 8546 function balanceAccordionLayouts(scope) { 8547 toArray((scope || document).querySelectorAll('.enisa-accordion')).forEach(function (wrapper) { 8548 toArray(wrapper.querySelectorAll('.enisa-accordion__grid')).forEach(function (grid) { 8549 applyBalancedLayout(grid, '.enisa-accordion__card', 'is-layout-half', 'is-layout-full', 'is-layout-third'); 8550 }); 8551 8552 toArray(wrapper.querySelectorAll('.enisa-accordion__downloads')).forEach(function (downloads) { 8553 applyBalancedLayout(downloads, '.enisa-accordion__download-btn', 'is-layout-half', 'is-layout-full', 'is-layout-third'); 8554 }); 8555 }); 8556 } 8557 8558 function initAccordionGridCarousels(panel) { 8559 if (!panel) return; 8560 8561 toArray(panel.querySelectorAll('.enisa-accordion__grid')).forEach(function (grid) { 8562 if (grid.getAttribute('data-accord-carousel-init') === 'true') return; 8563 8564 grid.setAttribute('data-accord-carousel-init', 'true'); 8565 8566 var cards = toArray(grid.querySelectorAll('.enisa-accordion__card')); 8567 8568 if (cards.length < 2 || !grid.parentNode) return; 8569 8570 var controls = document.createElement('div'); 8571 controls.className = 'enisa-editorial-row-controls'; 8572 controls.setAttribute('role', 'group'); 8573 controls.setAttribute('aria-label', 'Controles del carrusel'); 8574 8575 var prevBtn = document.createElement('button'); 8576 prevBtn.type = 'button'; 8577 prevBtn.className = 'enisa-editorial-row-controls__btn is-prev'; 8578 prevBtn.setAttribute('aria-label', 'Tarjeta anterior del acordeón'); 8579 prevBtn.innerHTML = '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 8580 8581 var dotsEl = document.createElement('div'); 8582 dotsEl.className = 'enisa-editorial-row-controls__dots'; 8583 dotsEl.setAttribute('role', 'group'); 8584 dotsEl.setAttribute('aria-label', 'Páginas del carrusel'); 8585 8586 var nextBtn = document.createElement('button'); 8587 nextBtn.type = 'button'; 8588 nextBtn.className = 'enisa-editorial-row-controls__btn is-next'; 8589 nextBtn.setAttribute('aria-label', 'Tarjeta siguiente del acordeón'); 8590 nextBtn.innerHTML = '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 8591 8592 var dots = cards.map(function (_, i) { 8593 var dot = document.createElement('button'); 8594 8595 dot.type = 'button'; 8596 dot.className = 'enisa-editorial-row-controls__dot'; 8597 dot.setAttribute('aria-label', 'Ir a tarjeta ' + (i + 1) + ' de ' + cards.length); 8598 dot.setAttribute('aria-current', i === 0 ? 'true' : 'false'); 8599 8600 if (i === 0) dot.classList.add('is-active'); 8601 8602 dotsEl.appendChild(dot); 8603 8604 return dot; 8605 }); 8606 8607 controls.appendChild(prevBtn); 8608 controls.appendChild(dotsEl); 8609 controls.appendChild(nextBtn); 8610 grid.parentNode.insertBefore(controls, grid.nextSibling); 8611 controls.setAttribute('hidden', ''); 8612 8613 function getActiveIndex() { 8614 var scrollLeft = grid.scrollLeft; 8615 var best = 0; 8616 var bestDist = Infinity; 8617 8618 cards.forEach(function (card, index) { 8619 var dist = Math.abs(card.offsetLeft - scrollLeft); 8620 8621 if (dist < bestDist) { 8622 bestDist = dist; 8623 best = index; 8624 } 8625 }); 8626 8627 return best; 8628 } 8629 8630 function setActiveDot(index) {
8631 dots.forEach(function (dot, dotIndex) { 8632 var active = dotIndex === index; 8633 8634 dot.classList.toggle('is-active', active); 8635 dot.setAttribute('aria-current', active ? 'true' : 'false'); 8636 dot.setAttribute('tabindex', active ? '0' : '-1'); 8637 }); 8638 8639 prevBtn.disabled = index === 0; 8640 nextBtn.disabled = index === cards.length - 1; 8641 } 8642 8643 function scrollToCard(index) { 8644 if (index < 0 || index >= cards.length) return; 8645 8646 grid.scrollTo({ 8647 left: cards[index].offsetLeft, 8648 behavior: prefersReducedMotion ? 'auto' : 'smooth' 8649 }); 8650 8651 setActiveDot(index); 8652 } 8653 8654 function shouldUseCarousel() { 8655 if ((window.innerWidth || 0) < 768) return true; 8656 8657 var styles = window.getComputedStyle ? window.getComputedStyle(grid) : null; 8658 var minCardWidth = styles ? parseFloat(styles.getPropertyValue('--enisa-accordion-carousel-min-card-w') || '268') : 268; 8659 var gap = styles ? parseFloat(styles.getPropertyValue('--enisa-accordion-grid-gap') || '16') : 16; 8660 var availableWidth = Math.floor(grid.getBoundingClientRect().width || grid.clientWidth || 0); 8661 8662 return availableWidth > 0 && ((minCardWidth * 2) + gap) > availableWidth + 1; 8663 } 8664 8665 function syncCarouselState() { 8666 var enabled = shouldUseCarousel(); 8667 8668 grid.classList.toggle('is-carousel', enabled); 8669 8670 if (enabled) { 8671 controls.removeAttribute('hidden'); 8672 8673 toArray(controls.querySelectorAll('button')).forEach(function (button) { 8674 button.removeAttribute('tabindex'); 8675 }); 8676
8677 setActiveDot(getActiveIndex()); 8678 return; 8679 } 8680 8681 grid.scrollLeft = 0; 8682 dotsEl.scrollLeft = 0; 8683 controls.setAttribute('hidden', ''); 8684 8685 toArray(controls.querySelectorAll('button')).forEach(function (button) { 8686 button.setAttribute('tabindex', '-1'); 8687 }); 8688 8689 setActiveDot(0); 8690 } 8691 8692 dots.forEach(function (dot, index) { 8693 dot.addEventListener('click', function () { 8694 scrollToCard(index); 8695 }); 8696 8697 dot.addEventListener('keydown', function (event) { 8698 var nextIndex = index; 8699 8700 switch (event.key) { 8701 case 'ArrowRight': 8702 case 'ArrowDown': 8703 nextIndex = (index + 1) % dots.length; 8704 break; 8705 case 'ArrowLeft': 8706 case 'ArrowUp': 8707 nextIndex = (index - 1 + dots.length) % dots.length; 8708 break; 8709 case 'Home': 8710 nextIndex = 0; 8711 break; 8712 case 'End': 8713 nextIndex = dots.length - 1; 8714 break; 8715 default: 8716 return; 8717 } 8718 8719 event.preventDefault(); 8720 scrollToCard(nextIndex); 8721 safeFocus(dots[nextIndex]); 8722 }); 8723 }); 8724 8725 prevBtn.addEventListener('click', function () { 8726 scrollToCard(getActiveIndex() - 1); 8727 }); 8728 8729 nextBtn.addEventListener('click', function () { 8730 scrollToCard(getActiveIndex() + 1); 8731 }); 8732 8733 var scrollTimer = null; 8734 8735 grid.addEventListener('scroll', function () { 8736 if (!grid.classList.contains('is-carousel')) return; 8737 8738 clearTimeout(scrollTimer); 8739 8740 scrollTimer = setTimeout(function () {
8741 setActiveDot(getActiveIndex()); 8742 }, 80); 8743 }, { passive: true }); 8744 8745 syncCarouselState(); 8746 8747 window.addEventListener('resize', debounce(syncCarouselState, 150)); 8748 }); 8749 } 8750 8751 function initFinAccordions(scope) { 8752 toArray((scope || document).querySelectorAll('.enisa-accordion')).forEach(function (wrapper) { 8753 var buttons = toArray(wrapper.querySelectorAll(FIN_TRIGGER_SELECTOR)); 8754 8755 if (!buttons.length) return; 8756 8757 buttons.forEach(function (btn) { 8758 if (btn.getAttribute(INIT_FLAG) === 'true') return; 8759 8760 btn.setAttribute(INIT_FLAG, 'true'); 8761 8762 var panel = getControlledPanel(btn, wrapper); 8763 8764 if (!panel) return; 8765 8766 var isOpen = btn.getAttribute('aria-expanded') === 'true' && !panel.hasAttribute('hidden'); 8767 8768 setVisualState(btn, panel, isOpen); 8769 setPanelInteractive(panel, isOpen); 8770 8771 btn.addEventListener('click', function () { 8772 var isOpen = btn.getAttribute('aria-expanded') === 'true'; 8773 8774 if (isOpen) { 8775 closeButton(btn, wrapper); 8776 return; 8777 } 8778 8779 openButton(btn, wrapper); 8780 }); 8781 }); 8782 8783 buttons.forEach(function (btn) { 8784 var panel = getControlledPanel(btn, wrapper); 8785 8786 if (!panel) return; 8787 8788 if (btn.getAttribute('aria-expanded') === 'true' && !panel.hasAttribute('hidden')) { 8789 panel.removeAttribute('hidden'); 8790 setVisualState(btn, panel, true); 8791 setPanelInteractive(panel, true); 8792 initAccordionGridCarousels(panel); 8793 } 8794 }); 8795 }); 8796 } 8797 8798 function getFaqRoot(btn) { 8799 return btn 8800 ? (btn.closest('[data-cmp-faqs], [data-cmp-faq], .enisa-faqs, .enisa-faq') || document) 8801 : document; 8802 } 8803 8804 function closeFaqItem(itemOrBtn, root) { 8805 var btn = itemOrBtn && itemOrBtn.matches && itemOrBtn.matches(FAQ_TRIGGER_SELECTOR) 8806 ? itemOrBtn 8807 : itemOrBtn && itemOrBtn.querySelector 8808 ? itemOrBtn.querySelector(FAQ_TRIGGER_SELECTOR) 8809 : null; 8810 8811 if (!btn) return; 8812 8813 closeButton(btn, root || getFaqRoot(btn)); 8814 } 8815 8816 function initFaqAccordions(scope) { 8817 var buttons = toArray((scope || document).querySelectorAll(FAQ_TRIGGER_SELECTOR)); 8818 8819 buttons.forEach(function (btn) { 8820 if (btn.getAttribute(INIT_FLAG) === 'true') return; 8821 8822 btn.setAttribute(INIT_FLAG, 'true'); 8823 8824 var root = getFaqRoot(btn); 8825 var panel = getControlledPanel(btn, root); 8826 8827 if (!panel) return; 8828 8829 var isOpen = btn.getAttribute('aria-expanded') === 'true' && !panel.hasAttribute('hidden'); 8830 8831 setVisualState(btn, panel, isOpen); 8832 setPanelInteractive(panel, isOpen); 8833 8834 btn.addEventListener('click', function () { 8835 var isOpen = btn.getAttribute('aria-expanded') === 'true'; 8836 8837 if (isOpen) { 8838 closeButton(btn, root); 8839 return; 8840 } 8841 8842 openButton(btn, root); 8843 }); 8844 }); 8845 } 8846 8847 function normalizeText(value) { 8848 return (value || '').toString().toLowerCase() 8849 .replace(/[áà äâ]/g, 'a') 8850 .replace(/[éèëê]/g, 'e') 8851 .replace(/[Ãìïî]/g, 'i') 8852 .replace(/[óòöô]/g, 'o') 8853 .replace(/[úùüû]/g, 'u') 8854 .replace(/ñ/g, 'n') 8855 .replace(/\s+/g, ' ') 8856 .trim(); 8857 } 8858 8859 function initFaqFilters(scope) { 8860 toArray((scope || document).querySelectorAll('[data-cmp-faqs], [data-cmp-faq]')).forEach(function (root) { 8861 if (root.getAttribute(FAQS_INIT_FLAG) === 'true') return; 8862 8863 root.setAttribute(FAQS_INIT_FLAG, 'true'); 8864 8865 var searchInput = root.querySelector('[data-faq-search]'); 8866 var clearBtn = root.querySelector('[data-search-clear]'); 8867 var searchIcon = root.querySelector('[data-search-icon]'); 8868 var categoryButtons = toArray(root.querySelectorAll('[data-filter]')); 8869 var items = toArray(root.querySelectorAll('[data-faq-item]')); 8870 var pagination = root.querySelector('[data-pagination]'); 8871 var pageSize = parseInt(root.getAttribute('data-page-size'), 10) || 6; 8872 var activeCategory = normalizeText(root.getAttribute('data-default-category') || ''); 8873 var searchTerm = ''; 8874 var currentPage = 1; 8875 var hasRendered = false;
8876 var status = document.createElement('p'); 8877 8878 status.className = 'enisa-sr-only'; 8879 status.setAttribute('role', 'status'); 8880 status.setAttribute('aria-live', 'polite'); 8881 status.setAttribute('aria-atomic', 'true'); 8882 root.appendChild(status); 8883 8884 if (!items.length) return; 8885 8886 var availableCategories = categoryButtons 8887 .map(function (btn) { 8888 return normalizeText(btn.getAttribute('data-filter') || ''); 8889 }) 8890 .filter(function (value, index, arr) { 8891 return value && arr.indexOf(value) === index; 8892 }); 8893 8894 if (!activeCategory && availableCategories.indexOf('financiacion') > -1) activeCategory = 'financiacion'; 8895 if (!activeCategory && availableCategories.length) activeCategory = availableCategories[0]; 8896 if (activeCategory && availableCategories.length && availableCategories.indexOf(activeCategory) === -1) activeCategory = availableCategories[0]; 8897 8898 function applyCategoryState() { 8899 categoryButtons.forEach(function (btn) { 8900 var key = normalizeText(btn.getAttribute('data-filter') || ''); 8901 var isActive = !searchTerm && key === activeCategory; 8902 8903 btn.classList.toggle('is-active', isActive); 8904 btn.setAttribute('aria-pressed', isActive ? 'true' : 'false'); 8905 }); 8906 } 8907 8908 function getFilteredItems() { 8909 return items.filter(function (item) { 8910 var itemCategory = normalizeText(item.getAttribute('data-faq-category') || ''); 8911 var question = normalizeText(item.getAttribute('data-faq-question') || item.textContent || ''); 8912 var answerNode = item.querySelector('.enisa-faq__answer, .enisa-faqs__answer'); 8913 var answerText = normalizeText(answerNode ? answerNode.textContent : ''); 8914 var matchesCategory = searchTerm ? true : (!activeCategory || itemCategory === activeCategory); 8915 var matchesSearch = !searchTerm || question.indexOf(searchTerm) > -1 || answerText.indexOf(searchTerm) > -1; 8916 8917 return matchesCategory && matchesSearch; 8918 }); 8919 } 8920 8921 function renderPagination(totalPages) { 8922 if (!pagination) return; 8923 8924 pagination.innerHTML = ''; 8925 8926 if (totalPages <= 1) { 8927 pagination.setAttribute('hidden', ''); 8928 return; 8929 } 8930 8931 pagination.removeAttribute('hidden'); 8932 8933 var prevBtn = document.createElement('button'); 8934 prevBtn.type = 'button'; 8935 prevBtn.className = 'enisa-pagination__btn enisa-pagination__btn--arrow is-prev'; 8936 prevBtn.setAttribute('aria-label', 'Página anterior'); 8937 prevBtn.innerHTML = '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 8938 prevBtn.disabled = currentPage <= 1; 8939 8940 prevBtn.addEventListener('click', function () { 8941 if (currentPage <= 1) return; 8942 8943 currentPage -= 1; 8944 render({ type: 'prev' }); 8945 }); 8946 8947 pagination.appendChild(prevBtn); 8948 8949 var pageList = document.createElement('div'); 8950 pageList.className = 'enisa-pagination__list'; 8951 pageList.setAttribute('role', 'list'); 8952 8953 for (var i = 1; i <= totalPages; i += 1) { 8954 var pageBtn = document.createElement('button'); 8955 8956 pageBtn.type = 'button'; 8957 pageBtn.className = 'enisa-pagination__btn' + (i === currentPage ? ' is-active' : ''); 8958 pageBtn.textContent = String(i); 8959 pageBtn.setAttribute('aria-label', 'Página ' + i); 8960 if (i === currentPage) { 8961 pageBtn.setAttribute('aria-current', 'page'); 8962 } 8963 8964 (function (pageNumber) { 8965 pageBtn.addEventListener('click', function () { 8966 currentPage = pageNumber; 8967 render({ type: 'page', page: pageNumber }); 8968 }); 8969 })(i); 8970 8971 pageList.appendChild(pageBtn); 8972 } 8973 8974 pagination.appendChild(pageList); 8975 8976 var nextBtn = document.createElement('button'); 8977 nextBtn.type = 'button'; 8978 nextBtn.className = 'enisa-pagination__btn enisa-pagination__btn--arrow is-next';
8979 nextBtn.setAttribute('aria-label', 'Página siguiente'); 8980 nextBtn.innerHTML = '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 8981 nextBtn.disabled = currentPage >= totalPages; 8982 8983 nextBtn.addEventListener('click', function () { 8984 if (currentPage >= totalPages) return; 8985 8986 currentPage += 1; 8987 render({ type: 'next' }); 8988 }); 8989 8990 pagination.appendChild(nextBtn); 8991 } 8992 8993 function getPaginationFocusTarget(focusTarget) { 8994 if (!focusTarget || !pagination) return null; 8995 8996 if (focusTarget.type === 'prev') { 8997 return pagination.querySelector('.enisa-pagination__btn--arrow.is-prev'); 8998 } 8999 9000 if (focusTarget.type === 'next') { 9001 return pagination.querySelector('.enisa-pagination__btn--arrow.is-next'); 9002 } 9003 9004 if (focusTarget.type === 'page') { 9005 return toArray(pagination.querySelectorAll('.enisa-pagination__list .enisa-pagination__btn')).filter(function (button) { 9006 return button.textContent === String(focusTarget.page); 9007 })[0] || null; 9008 } 9009 9010 return null; 9011 } 9012 9013 function render(focusTarget) { 9014 var filtered = getFilteredItems(); 9015 var totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); 9016 var activeItem = document.activeElement && document.activeElement.closest 9017 ? document.activeElement.closest('[data-faq-item]') 9018 : null; 9019 var start; 9020 var end; 9021 var visibleSlice; 9022 9023 if (currentPage > totalPages) currentPage = totalPages; 9024 if (currentPage < 1) currentPage = 1; 9025 9026 start = (currentPage - 1) * pageSize; 9027 end = start + pageSize; 9028 visibleSlice = filtered.slice(start, end); 9029 9030 if (activeItem && visibleSlice.indexOf(activeItem) === -1) { 9031 var fallbackFocus = getPaginationFocusTarget(focusTarget) || 9032 searchInput || 9033 categoryButtons.filter(function (button) { 9034 return button.getAttribute('aria-pressed') === 'true'; 9035 })[0] || 9036 categoryButtons[0]; 9037 9038 if ( 9039 fallbackFocus && 9040 window.ENISA && 9041 window.ENISA.utils && 9042 typeof window.ENISA.utils.focusWithoutScroll === 'function' 9043 ) { 9044 window.ENISA.utils.focusWithoutScroll(fallbackFocus); 9045 } else if (fallbackFocus && typeof fallbackFocus.focus === 'function') { 9046 safeFocus(fallbackFocus); 9047 } 9048 } 9049 9050 items.forEach(function (item) { 9051 var visible = visibleSlice.indexOf(item) > -1; 9052 9053 item.hidden = !visible; 9054 9055 if (!visible) { 9056 closeFaqItem(item, root); 9057 } 9058 }); 9059 9060 renderPagination(totalPages); 9061 applyCategoryState(); 9062 9063 if (hasRendered) { 9064 status.textContent = ''; 9065 setTimeout(function () { 9066 status.textContent = filtered.length + (filtered.length === 1 ? ' pregunta disponible.' : ' preguntas disponibles.'); 9067 }, 20); 9068 } 9069 9070 hasRendered = true; 9071 9072 if (focusTarget) { 9073 window.requestAnimationFrame(function () { 9074 var nextFocusTarget = getPaginationFocusTarget(focusTarget); 9075 9076 if (nextFocusTarget && typeof nextFocusTarget.focus === 'function' && document.contains(nextFocusTarget)) { 9077 safeFocus(nextFocusTarget); 9078 } 9079 }); 9080 } 9081 9082 if (searchIcon) searchIcon.hidden = !!searchTerm; 9083 9084 if (clearBtn) { 9085 clearBtn.hidden = !searchTerm; 9086 } 9087 } 9088 9089 categoryButtons.forEach(function (btn) { 9090 btn.addEventListener('click', function () { 9091 activeCategory = normalizeText(btn.getAttribute('data-filter') || ''); 9092 currentPage = 1; 9093 applyCategoryState(); 9094 render(); 9095 }); 9096 }); 9097 9098 if (searchInput) { 9099 searchInput.addEventListener('input', function () { 9100 searchTerm = normalizeText(searchInput.value || ''); 9101 currentPage = 1; 9102 render(); 9103 }); 9104 } 9105 9106 if (clearBtn && searchInput) { 9107 clearBtn.addEventListener('click', function () { 9108 searchInput.value = ''; 9109 searchTerm = ''; 9110 currentPage = 1; 9111 safeFocus(searchInput); 9112 render(); 9113 }); 9114 } 9115 9116 applyCategoryState(); 9117 render(); 9118 }); 9119 } 9120 9121 function init(scope) { 9122 var currentScope = scope || document; 9123 9124 balanceAccordionLayouts(currentScope); 9125 initFinAccordions(currentScope); 9126 initFaqAccordions(currentScope); 9127 initFaqFilters(currentScope); 9128 } 9129 9130 if (document.readyState === 'loading') { 9131 document.addEventListener('DOMContentLoaded', function () { 9132 init(document); 9133 }); 9134 } else { 9135 init(document); 9136 } 9137 9138 window.addEventListener('resize', debounce(function () { 9139 balanceAccordionLayouts(document); 9140 }, 150)); 9141 9142 if ('MutationObserver' in window) { 9143 var timer = null; 9144 var observer = new MutationObserver(function (mutations) { 9145 var shouldInit = mutations.some(function (mutation) { 9146 return toArray(mutation.addedNodes).some(function (node) { 9147 if (!node || node.nodeType !== 1) return false;
9148 9149 return ( 9150 (node.matches && 9151 ( 9152 node.matches('.enisa-accordion, [data-cmp-faqs], [data-cmp-faq]') || 9153 node.matches(FIN_TRIGGER_SELECTOR) || 9154 node.matches(FAQ_TRIGGER_SELECTOR) 9155 ) 9156 ) || 9157 (node.querySelector && 9158 ( 9159 node.querySelector('.enisa-accordion, [data-cmp-faqs], [data-cmp-faq]') || 9160 node.querySelector(FIN_TRIGGER_SELECTOR) || 9161 node.querySelector(FAQ_TRIGGER_SELECTOR) 9162 ) 9163 ) 9164 ); 9165 }); 9166 }); 9167 9168 if (!shouldInit) return; 9169 9170 clearTimeout(timer); 9171 9172 timer = setTimeout(function () { 9173 init(document); 9174 }, 150); 9175 }); 9176 9177 observer.observe(document.body, { 9178 childList: true, 9179 subtree: true 9180 }); 9181 } 9182})(); 9183 9184/* 9185 ENISA | JS Module 9186 Modulo: 21-tables 9187 Descripcion: Convierte tablas semanticas en cards mobile con carrusel accesible. 9188*/ 9189(function () { 9190 "use strict"; 9191 9192 var TABLE_SELECTOR = 9193 "table[data-enisa-table], .enisa-prose table:not([data-enisa-table-disabled])"; 9194 9195 var INIT_ATTR = "data-enisa-table-init"; 9196 var MOBILE_BP = 9197 window.ENISA && window.ENISA.cardsCarousel 9198 ? window.ENISA.cardsCarousel.MOBILE_BP 9199 : 768; 9200 9201 function isMobileViewport() { 9202 if (window.matchMedia) { 9203 return window.matchMedia("(max-width: " + MOBILE_BP + "px)").matches; 9204 } 9205 9206 return window.innerWidth <= MOBILE_BP; 9207 } 9208 9209 function normalizeText(value) { 9210 return (value || "").toString().replace(/\s+/g, " ").trim(); 9211 } 9212 9213 function getHeaders(table) { 9214 var headers = [].slice.call(table.querySelectorAll("thead th")); 9215 9216 return headers.map(function (header, index) { 9217 var text = normalizeText(header.textContent); 9218 return text || "Columna " + (index + 1); 9219 }); 9220 } 9221 9222 function getTableLabel(table) { 9223 var caption = table.querySelector("caption"); 9224 var captionText = caption ? normalizeText(caption.textContent) : ""; 9225 9226 if (captionText) return captionText; 9227 9228 var ariaLabel = normalizeText(table.getAttribute("aria-label")); 9229 if (ariaLabel) return ariaLabel; 9230 9231 return "tabla"; 9232 } 9233 9234 function hasInteractiveContent(cell) { 9235 return !!cell.querySelector("a[href], button, input, select, textarea"); 9236 } 9237 9238 function isActionHeader(label) { 9239 var normalized = normalizeText(label).toLowerCase(); 9240 9241 return ( 9242 normalized === "acción" || 9243 normalized === "accion" || 9244 normalized === "descarga" || 9245 normalized === "descargar" || 9246 normalized === "enlace" 9247 ); 9248 } 9249 9250 function isRowVisible(row) { 9251 if (!row) return false; 9252 if (row.hidden) return false; 9253 if (row.hasAttribute("hidden")) return false; 9254 if (row.style && row.style.display === "none") return false; 9255 9256 return true; 9257 } 9258 9259 function createControls(total, label) { 9260 var controls = document.createElement("div"); 9261 controls.className = "enisa-editorial-row-controls"; 9262 controls.setAttribute( 9263 "aria-label", 9264 label || "Controles del carrusel de tabla", 9265 ); 9266 9267 var prevBtn = document.createElement("button"); 9268 prevBtn.type = "button"; 9269 prevBtn.className = "enisa-editorial-row-controls__btn is-prev"; 9270 prevBtn.setAttribute("aria-label", "Fila anterior"); 9271 prevBtn.innerHTML = 9272 '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 9273 9274 var dotsEl = document.createElement("div"); 9275 dotsEl.className = "enisa-editorial-row-controls__dots"; 9276 dotsEl.setAttribute("role", "tablist"); 9277 dotsEl.setAttribute("aria-label", "Páginas del carrusel"); 9278 9279 var nextBtn = document.createElement("button"); 9280 nextBtn.type = "button"; 9281 nextBtn.className = "enisa-editorial-row-controls__btn is-next"; 9282 nextBtn.setAttribute("aria-label", "Fila siguiente"); 9283 nextBtn.innerHTML = 9284 '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 9285 9286 for (var i = 0; i < total; i += 1) { 9287 var dot = document.createElement("button"); 9288 9289 dot.type = "button"; 9290 dot.className = "enisa-editorial-row-controls__dot"; 9291 dot.setAttribute("role", "tab"); 9292 dot.setAttribute("aria-label", "Ir a fila " + (i + 1)); 9293 dot.setAttribute("aria-selected", i === 0 ? "true" : "false"); 9294 9295 if (i === 0) { 9296 dot.classList.add("is-active"); 9297 } 9298 9299 dotsEl.appendChild(dot); 9300 } 9301 9302 controls.appendChild(prevBtn); 9303 controls.appendChild(dotsEl); 9304 controls.appendChild(nextBtn); 9305 9306 return { 9307 root: controls, 9308 prevBtn: prevBtn, 9309 nextBtn: nextBtn, 9310 dots: [].slice.call( 9311 dotsEl.querySelectorAll(".enisa-editorial-row-controls__dot"), 9312 ), 9313 }; 9314 } 9315 9316 function cloneCellContent(cell) { 9317 var fragment = document.createDocumentFragment(); 9318
9319 [].slice.call(cell.childNodes).forEach(function (node) { 9320 fragment.appendChild(node.cloneNode(true)); 9321 }); 9322 9323 return fragment; 9324 } 9325 9326 function buildMobileCards(table, wrapper, viewport, track, rows, headers) { 9327 var tableLabel = getTableLabel(table); 9328 9329 track.innerHTML = ""; 9330 9331 rows.forEach(function (row, rowIndex) { 9332 var cells = [].slice.call(row.children); 9333 var card = document.createElement("article"); 9334 9335 card.className = "enisa-table-card"; 9336 card.setAttribute("data-enisa-table-card", ""); 9337 card.setAttribute( 9338 "aria-label", 9339 "Fila " + (rowIndex + 1) + " de " + tableLabel, 9340 ); 9341 9342 cells.forEach(function (cell, cellIndex) { 9343 var labelText = headers[cellIndex] || "Columna " + (cellIndex + 1); 9344 var isAction = isActionHeader(labelText) || hasInteractiveContent(cell); 9345 9346 var item = document.createElement("div"); 9347 item.className = "enisa-table-card__item"; 9348 9349 if (cellIndex === 0) { 9350 item.classList.add("enisa-table-card__item--primary"); 9351 } 9352 9353 if (isAction) { 9354 item.classList.add("enisa-table-card__item--action"); 9355 } 9356 9357 var label = document.createElement("span"); 9358 label.className = "enisa-table-card__label"; 9359 label.textContent = labelText; 9360 9361 var value = document.createElement("div"); 9362 value.className = "enisa-table-card__value"; 9363 value.appendChild(cloneCellContent(cell)); 9364 9365 if (!isAction) { 9366 item.appendChild(label); 9367 } 9368 9369 item.appendChild(value); 9370 card.appendChild(item); 9371 }); 9372 9373 track.appendChild(card); 9374 }); 9375 9376 wrapper.hidden = !isMobileViewport(); 9377 } 9378 9379 function initTable(table) { 9380 if (!table || table.getAttribute(INIT_ATTR) === "true") return; 9381 9382 var headers = getHeaders(table); 9383 var rows = [].slice.call(table.querySelectorAll("tbody tr")); 9384 9385 if (!headers.length || !rows.length) return; 9386 9387 table.setAttribute(INIT_ATTR, "true"); 9388 table.classList.add("enisa-table"); 9389 table.classList.add("has-enisa-table-source"); 9390 9391 if (table.parentElement) { 9392 table.parentElement.classList.add("has-enisa-table-cards"); 9393 } 9394 9395 var wrapper = document.createElement("div"); 9396 wrapper.className = "enisa-table-mobile"; 9397 wrapper.setAttribute("data-enisa-table-mobile", ""); 9398 wrapper.hidden = true; 9399 9400 var viewport = document.createElement("div"); 9401 viewport.className = "enisa-table-mobile__viewport"; 9402 9403 var track = document.createElement("div"); 9404 track.className = "enisa-table-mobile__track"; 9405 9406 viewport.appendChild(track); 9407 wrapper.appendChild(viewport); 9408 table.insertAdjacentElement("afterend", wrapper); 9409 9410 var controlsApi = null; 9411 var carouselApi = null; 9412 9413 function removeControls() { 9414 if (controlsApi && controlsApi.root && controlsApi.root.parentNode) { 9415 controlsApi.root.parentNode.removeChild(controlsApi.root); 9416 } 9417 9418 controlsApi = null; 9419 carouselApi = null; 9420 } 9421 9422 function getCards() { 9423 return [].slice.call(track.querySelectorAll("[data-enisa-table-card]")); 9424 } 9425 9426 function getVisibleCards() { 9427 var cards = getCards(); 9428 9429 return cards.filter(function (card, index) { 9430 var row = rows[index]; 9431 var visible = isRowVisible(row); 9432 9433 card.hidden = !visible; 9434 9435 return visible; 9436 }); 9437 } 9438 9439 function rebuildControls() { 9440 removeControls(); 9441 9442 if (!isMobileViewport()) { 9443 wrapper.hidden = true; 9444 return; 9445 } 9446 9447 var visibleCards = getVisibleCards(); 9448 9449 wrapper.hidden = visibleCards.length === 0; 9450 9451 if (visibleCards.length < 2) return; 9452 9453 controlsApi = createControls( 9454 visibleCards.length, 9455 table.getAttribute("data-enisa-table-controls-label") || 9456 "Controles del carrusel de tabla", 9457 ); 9458 9459 wrapper.appendChild(controlsApi.root); 9460 9461 if ( 9462 window.ENISA && 9463 window.ENISA.utils && 9464 typeof window.ENISA.utils.createScrollCarousel === "function" 9465 ) { 9466 carouselApi = window.ENISA.utils.createScrollCarousel({ 9467 viewport: viewport, 9468 items: visibleCards, 9469 prevBtn: controlsApi.prevBtn, 9470 nextBtn: controlsApi.nextBtn, 9471 dots: controlsApi.dots, 9472 activeDotClass: "is-active", 9473 }); 9474 } 9475 } 9476 9477 function sync() { 9478 if (!isMobileViewport()) { 9479 wrapper.hidden = true; 9480 removeControls(); 9481 return; 9482 } 9483 9484 getVisibleCards(); 9485 rebuildControls(); 9486 9487 if (carouselApi && typeof carouselApi.sync === "function") { 9488 carouselApi.sync(); 9489 } 9490 } 9491 9492 buildMobileCards(table, wrapper, viewport, track, rows, headers); 9493 9494 table.enisaTableSync = sync; 9495 9496 sync(); 9497 9498 window.addEventListener("load", sync, { once: true }); 9499 9500 if ( 9501 window.ENISA && 9502 window.ENISA.utils && 9503 typeof window.ENISA.utils.debounce === "function" 9504 ) { 9505 window.addEventListener("resize", window.ENISA.utils.debounce(sync, 150)); 9506 } else { 9507 window.addEventListener("resize", sync); 9508 } 9509 } 9510 9511 function syncTables(scope) { 9512 var root = scope || document; 9513 var tables = []; 9514 9515 if (root.tagName && root.tagName.toLowerCase() === "table") { 9516 tables = [root]; 9517 } else { 9518 tables = [].slice.call( 9519 root.querySelectorAll( 9520 "table.has-enisa-table-source, table[data-enisa-table], .enisa-prose table:not([data-enisa-table-disabled])", 9521 ), 9522 ); 9523 } 9524
9525 tables.forEach(function (table) { 9526 if (typeof table.enisaTableSync === "function") { 9527 table.enisaTableSync(); 9528 } 9529 }); 9530 } 9531 9532 function init(scope) { 9533 var root = scope || document; 9534 var tables = [].slice.call(root.querySelectorAll(TABLE_SELECTOR)); 9535 9536 tables.forEach(initTable); 9537 } 9538 9539 window.ENISA = window.ENISA || {}; 9540 window.ENISA.tables = window.ENISA.tables || {}; 9541 window.ENISA.tables.init = init; 9542 window.ENISA.tables.sync = syncTables; 9543 9544 if (document.readyState === "loading") { 9545 document.addEventListener("DOMContentLoaded", function () { 9546 init(document); 9547 }); 9548 } else { 9549 init(document); 9550 } 9551})(); 9552 9553/* 9554 ENISA | JS Module 9555 Modulo: 22-search 9556 Descripcion: Busqueda global, ranking por relevancia, paginacion y resaltado. 9557 Nota: 9558 - No abre acordeones. 9559 - No activa tabs. 9560 - Restaura una sola vez el foco al contexto de resultados tras una busqueda iniciada por el usuario. 9561 - Conserva el foco en la paginacion tras cambiar de pagina. 9562 - No interpreta destinos. 9563 - La navegacion usa directamente el href definido en Dataverse. 9564*/ 9565(function () { 9566 "use strict"; 9567 9568 var SEARCH_FOCUS_INTENT_KEY = "enisa:global-search:focus-results"; 9569 var STATUS_ANNOUNCEMENT_DELAY = 80; 9570 9571 function safeFocus(element, options) { 9572 var opts = options || {}; 9573 9574 if ( 9575 window.ENISA && 9576 window.ENISA.utils && 9577 typeof window.ENISA.utils.safeFocus === "function" 9578 ) { 9579 return window.ENISA.utils.safeFocus(element, opts); 9580 } 9581 9582 if (!element || typeof element.focus !== "function") return false; 9583 9584 try { 9585 if (opts.preventScroll === false) { 9586 element.focus(); 9587 } else { 9588 element.focus({ preventScroll: true }); 9589 } 9590 } catch (error) { 9591 element.focus(); 9592 } 9593 9594 return true; 9595 } 9596 9597 function scrollToElement(element, options) { 9598 if ( 9599 window.ENISA && 9600 window.ENISA.utils && 9601 typeof window.ENISA.utils.scrollToElement === "function" 9602 ) { 9603 return window.ENISA.utils.scrollToElement(element, options); 9604 } 9605 9606 if (!element || typeof element.scrollIntoView !== "function") { 9607 return false; 9608 } 9609 9610 try { 9611 element.scrollIntoView({ 9612 block: options && options.block ? options.block : "start", 9613 behavior: options && options.behavior ? options.behavior : "smooth" 9614 }); 9615 } catch (error) { 9616 element.scrollIntoView(); 9617 } 9618 9619 return true; 9620 } 9621 9622 function getVisibleRange(state) { 9623 var total = state && state.items ? state.items.length : 0; 9624 var totalPages; 9625 var currentPage; 9626 var start; 9627 var end; 9628 9629 if (!total) { 9630 return { 9631 start: 0, 9632 end: 0, 9633 total: 0 9634 }; 9635 } 9636 9637 totalPages = Math.max(1, Math.ceil(total / state.pageSize)); 9638 currentPage = Math.min(Math.max(state.currentPage, 1), totalPages); 9639 start = (currentPage - 1) * state.pageSize + 1; 9640 end = Math.min(currentPage * state.pageSize, total); 9641 9642 return { 9643 start: start, 9644 end: end, 9645 total: total 9646 }; 9647 } 9648 9649 function buildResultsMessage(total, term, state) { 9650 var safeTerm = normalizeSpaces(term); 9651 var range; 9652 9653 if (total === 0) { 9654 return "No se han encontrado resultados para â" + safeTerm + "â."; 9655 } 9656 9657 if (state && state.pageSize && total > state.pageSize) { 9658 range = getVisibleRange(state); 9659 9660 return ( 9661 "Mostrando resultados " + 9662 range.start + 9663 "-" + 9664 range.end + 9665 " de " + 9666 range.total + 9667 " para â" + 9668 safeTerm + 9669 "â." 9670 ); 9671 } 9672 9673 return ( 9674 "Se han encontrado " + 9675 total + 9676 " resultado" + 9677 (total !== 1 ? "s" : "") + 9678 " para â" + 9679 safeTerm + 9680 "â." 9681 ); 9682 } 9683 9684 function alignInitialHashTarget() { 9685 if (!window.location.hash) { 9686 return; 9687 } 9688 9689 var rawHash = window.location.hash.replace(/^#/, ""); 9690 9691 if (!rawHash) { 9692 return; 9693 } 9694 9695 try { 9696 rawHash = decodeURIComponent(rawHash); 9697 } catch (error) {} 9698 9699 var target = document.getElementById(rawHash); 9700 9701 if (!target) { 9702 return; 9703 } 9704 9705 function scrollToTarget() { 9706 scrollToElement(target, { 9707 block: "start", 9708 behavior: "auto", 9709 offset: 24 9710 }); 9711 } 9712 9713 var root = document.documentElement; 9714 var previousScrollBehavior = root.style.scrollBehavior; 9715 9716 root.style.scrollBehavior = "auto"; 9717 9718 window.requestAnimationFrame(function () { 9719 scrollToTarget(); 9720 9721 window.setTimeout(function () { 9722 scrollToTarget(); 9723 9724 root.style.scrollBehavior = previousScrollBehavior; 9725 }, 80); 9726 }); 9727 } 9728 9729 var SELECTORS = { 9730 root: "[data-global-search]", 9731 form: ".enisa-global-search__form", 9732 queryInput: ".enisa-global-search__input[name='q']", 9733 compatInput: "[data-global-search-compat-input]", 9734 item: "[data-global-search-item]", 9735 entryItem: ".enisa-global-search__result--entry", 9736 resultLink: "[data-global-search-result-link]", 9737 pagination: "[data-global-search-pagination]", 9738 results: "[data-global-search-results]", 9739 resultsTitle: "[data-global-search-results-title]", 9740 counter: "[data-global-search-counter]", 9741 status: "[data-global-search-status]", 9742 emptyResults: "[data-global-search-empty-results]", 9743 highlightTarget: "[data-search-highlight]" 9744 }; 9745 9746 var CLASSES = { 9747 highlight: "enisa-global-search__highlight",
9748 hiddenByPage: "is-hidden-by-page", 9749 paginationButton: "enisa-pagination__btn", 9750 paginationArrow: "enisa-pagination__btn--arrow", 9751 paginationList: "enisa-pagination__list", 9752 active: "is-active" 9753 }; 9754 9755 var DEFAULT_PAGE_SIZE = 10; 9756 var PAGINATION_WINDOW_SIZE = 3; 9757 var MIN_HIGHLIGHT_LENGTH = 2; 9758 9759 var STOP_WORDS = { 9760 a: true, 9761 al: true, 9762 de: true, 9763 del: true, 9764 el: true, 9765 en: true, 9766 es: true, 9767 la: true, 9768 las: true, 9769 lo: true, 9770 los: true, 9771 o: true, 9772 para: true, 9773 por: true, 9774 que: true, 9775 se: true, 9776 un: true, 9777 una: true, 9778 y: true 9779 }; 9780 9781 var ACCENT_GROUPS = { 9782 a: "aáà äâãåÄ", 9783 e: "eéèëêÄ", 9784 i: "iÃìïîī", 9785 o: "oóòöôõÅ", 9786 u: "uúùüûū", 9787 n: "nñ", 9788 c: "cç" 9789 }; 9790 9791 function escapeRegExp(value) { 9792 return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 9793 } 9794 9795 function normalizeSpaces(value) { 9796 return String(value || "").replace(/\s+/g, " ").trim(); 9797 } 9798 9799 function markSearchFocusIntent() { 9800 try { 9801 window.sessionStorage.setItem(SEARCH_FOCUS_INTENT_KEY, "1"); 9802 } catch (error) {} 9803 } 9804 9805 function consumeSearchFocusIntent() { 9806 var shouldRestoreFocus = false; 9807 9808 try { 9809 shouldRestoreFocus = 9810 window.sessionStorage.getItem(SEARCH_FOCUS_INTENT_KEY) === "1"; 9811 9812 window.sessionStorage.removeItem(SEARCH_FOCUS_INTENT_KEY); 9813 } catch (error) {} 9814 9815 return shouldRestoreFocus; 9816 } 9817 9818 function normalizeText(value) { 9819 return normalizeSpaces(value) 9820 .toLowerCase() 9821 .normalize("NFD") 9822 .replace(/[\u0300-\u036f]/g, ""); 9823 } 9824 9825 function getDatasetValue(item, name) { 9826 if (!item || !item.dataset) { 9827 return ""; 9828 } 9829 9830 return item.dataset[name] || ""; 9831 } 9832 9833 function getNumberDatasetValue(item, name, fallback) { 9834 var parsed = parseInt(getDatasetValue(item, name), 10); 9835 9836 return Number.isFinite(parsed) ? parsed : fallback; 9837 } 9838 9839 function getPageSize(root) { 9840 var pageSize = parseInt( 9841 root.getAttribute("data-global-search-page-size"), 9842 10 9843 ); 9844 9845 return Number.isFinite(pageSize) && pageSize > 0 9846 ? pageSize 9847 : DEFAULT_PAGE_SIZE; 9848 } 9849 9850 function getSearchTerm(root) { 9851 return normalizeSpaces(root.getAttribute("data-search-term") || ""); 9852 } 9853 9854 function syncCompatInput(root) { 9855 var queryInput = root.querySelector(SELECTORS.queryInput); 9856 var compatInput = root.querySelector(SELECTORS.compatInput); 9857 9858 if (!queryInput || !compatInput) { 9859 return; 9860 } 9861 9862 compatInput.value = normalizeSpaces(queryInput.value || ""); 9863 } 9864 9865 function bindForm(root) { 9866 var form = root.querySelector(SELECTORS.form); 9867 var queryInput = root.querySelector(SELECTORS.queryInput); 9868 9869 if (!form) { 9870 return; 9871 } 9872 9873 form.addEventListener("submit", function (event) { 9874 var query = queryInput ? normalizeSpaces(queryInput.value) : ""; 9875 9876 syncCompatInput(root); 9877 9878 if (!query) { 9879 event.preventDefault(); 9880 safeFocus(queryInput); 9881 return; 9882 } 9883 9884 if (event.isTrusted !== false) { 9885 markSearchFocusIntent(); 9886 } 9887 }); 9888 } 9889 9890 function isModifiedPointerEvent(event) { 9891 return Boolean( 9892 event && 9893 ( 9894 event.button !== 0 || 9895 event.metaKey || 9896 event.ctrlKey || 9897 event.shiftKey || 9898 event.altKey 9899 ) 9900 ); 9901 } 9902 9903 function isInteractiveTarget(target, root) { 9904 var interactive; 9905 9906 if (!target || !root || !target.closest) { 9907 return false; 9908 } 9909 9910 interactive = target.closest( 9911 "a[href], button, input, select, textarea, summary, [role='button'], [role='link']" 9912 ); 9913 9914 return Boolean(interactive && root.contains(interactive)); 9915 } 9916 9917 function bindEntryPointerNavigation(root) { 9918 var entries = [].slice.call(root.querySelectorAll(SELECTORS.entryItem)); 9919 9920 entries.forEach(function (entry) { 9921 if (entry.getAttribute("data-global-search-entry-init") === "true") { 9922 return; 9923 } 9924 9925 entry.setAttribute("data-global-search-entry-init", "true"); 9926 9927 entry.addEventListener("click", function (event) { 9928 var link; 9929 9930 if ( 9931 isModifiedPointerEvent(event) || 9932 isInteractiveTarget(event.target, entry) 9933 ) { 9934 return; 9935 } 9936 9937 link = entry.querySelector(SELECTORS.resultLink); 9938 9939 if (link && typeof link.click === "function") { 9940 link.click(); 9941 } 9942 }); 9943 }); 9944 } 9945 9946 function isDocumentFocusIdle() { 9947 var activeElement = document.activeElement; 9948 9949 return ( 9950 !activeElement || 9951 activeElement === document.body || 9952 activeElement === document.documentElement 9953 ); 9954 } 9955 9956 function focusResultsContext(root) { 9957 var resultsTitle = root.querySelector(SELECTORS.resultsTitle); 9958 9959 if ( 9960 !resultsTitle || 9961 !document.contains(resultsTitle) || 9962 !isDocumentFocusIdle() 9963 ) { 9964 return; 9965 } 9966 9967 window.requestAnimationFrame(function () { 9968 window.requestAnimationFrame(function () { 9969 if ( 9970 !document.contains(resultsTitle) || 9971 !isDocumentFocusIdle() 9972 ) { 9973 return; 9974 } 9975 9976 safeFocus(resultsTitle, { 9977 preventScroll: false 9978 }); 9979 }); 9980 }); 9981 } 9982 9983 function getPaginationFocusTarget(root, focusTarget) { 9984 var pagination = root.querySelector(SELECTORS.pagination); 9985 var currentPageButton; 9986 var requestedButton; 9987 9988 if (!pagination || !focusTarget) { 9989 return null; 9990 } 9991 9992 currentPageButton = pagination.querySelector('[aria-current="page"]'); 9993 9994 if (focusTarget.type === "prev") { 9995 requestedButton = pagination.querySelector( 9996 "." + CLASSES.paginationArrow + ".is-prev" 9997 ); 9998 } else if (focusTarget.type === "next") { 9999 requestedButton = pagination.querySelector( 10000 "." + CLASSES.paginationArrow + ".is-next" 10001 ); 10002 } else if (focusTarget.type === "page") { 10003 requestedButton = [].slice.call( 10004 pagination.querySelectorAll( 10005 "." + CLASSES.paginationList + " ." + CLASSES.paginationButton 10006 ) 10007 ).filter(function (button) { 10008 return button.textContent === String(focusTarget.page); 10009 })[0] || null; 10010 } 10011 10012 if (requestedButton && !requestedButton.disabled) { 10013 return requestedButton; 10014 } 10015 10016 return currentPageButton || null; 10017 } 10018 10019 function restorePaginationFocus(root, focusTarget) { 10020 window.requestAnimationFrame(function () { 10021 window.requestAnimationFrame(function () { 10022 var target = getPaginationFocusTarget(root, focusTarget); 10023 10024 if (target && document.contains(target)) { 10025 safeFocus(target); 10026 } 10027 }); 10028 }); 10029 } 10030 10031 function announceResultsUpdate(root, total, term, state) { 10032 var status = root.querySelector(SELECTORS.status); 10033 var message; 10034 10035 if (!status) { 10036 return; 10037 } 10038 10039 message = buildResultsMessage(total, term, state); 10040 10041 if (root.__enisaGlobalSearchStatusTimer) { 10042 window.clearTimeout(root.__enisaGlobalSearchStatusTimer); 10043 } 10044 10045 status.textContent = ""; 10046 10047 root.__enisaGlobalSearchStatusTimer = window.setTimeout(function () { 10048 status.textContent = message; 10049 root.__enisaGlobalSearchStatusTimer = null; 10050 }, STATUS_ANNOUNCEMENT_DELAY); 10051 } 10052 10053 function getSearchTokens(term) { 10054 var normalized = normalizeText(term); 10055 10056 return normalized 10057 .split(/[^a-z0-9ñ]+/i) 10058 .map(function (token) { 10059 return token.trim(); 10060 }) 10061 .filter(function (token) { 10062 if (!token) { 10063 return false;
10064 } 10065 10066 if (STOP_WORDS[token]) { 10067 return false; 10068 } 10069 10070 return token.length >= MIN_HIGHLIGHT_LENGTH; 10071 }); 10072 } 10073 10074 function getItemSearchParts(item) { 10075 return { 10076 title: normalizeText(getDatasetValue(item, "searchTitle")), 10077 subtitle: normalizeText(getDatasetValue(item, "searchSubtitle")), 10078 page: normalizeText(getDatasetValue(item, "searchPage")), 10079 summary: normalizeText(getDatasetValue(item, "searchSummary")), 10080 content: normalizeText(getDatasetValue(item, "searchContent")), 10081 keywords: normalizeText(getDatasetValue(item, "searchKeywords")), 10082 text: normalizeText(getDatasetValue(item, "searchText")) 10083 }; 10084 } 10085 10086 function containsAllTokens(text, tokens) { 10087 if (!tokens.length) { 10088 return false; 10089 } 10090 10091 return tokens.every(function (token) { 10092 return text.indexOf(token) !== -1; 10093 }); 10094 } 10095 10096 function containsAnyToken(text, tokens) { 10097 if (!tokens.length) { 10098 return false; 10099 } 10100 10101 return tokens.some(function (token) { 10102 return text.indexOf(token) !== -1; 10103 }); 10104 } 10105 10106 function itemMatches(parts, normalizedTerm, tokens) { 10107 if ( 10108 !normalizedTerm || 10109 normalizedTerm.length < MIN_HIGHLIGHT_LENGTH 10110 ) { 10111 return false; 10112 } 10113 10114 if (parts.text.indexOf(normalizedTerm) !== -1) { 10115 return true; 10116 } 10117 10118 if (containsAllTokens(parts.text, tokens)) { 10119 return true; 10120 } 10121 10122 if (tokens.length === 1) { 10123 return containsAnyToken( 10124 parts.title + " " + parts.keywords + " " + parts.summary, 10125 tokens 10126 ); 10127 } 10128 10129 return false; 10130 } 10131 10132 function scoreExactPhrase(parts, normalizedTerm) { 10133 var score = 0; 10134 10135 if (parts.title.indexOf(normalizedTerm) !== -1) { 10136 score += 260; 10137 } 10138 10139 if (parts.keywords.indexOf(normalizedTerm) !== -1) { 10140 score += 240; 10141 } 10142 10143 if (parts.subtitle.indexOf(normalizedTerm) !== -1) { 10144 score += 150; 10145 } 10146 10147 if (parts.summary.indexOf(normalizedTerm) !== -1) { 10148 score += 120; 10149 } 10150 10151 if (parts.page.indexOf(normalizedTerm) !== -1) { 10152 score += 90; 10153 } 10154 10155 if (parts.content.indexOf(normalizedTerm) !== -1) { 10156 score += 50; 10157 } 10158 10159 return score; 10160 } 10161 10162 function scoreTokens(parts, tokens) { 10163 var score = 0; 10164 10165 tokens.forEach(function (token) { 10166 if (parts.title.indexOf(token) !== -1) { 10167 score += 75; 10168 } 10169 10170 if (parts.keywords.indexOf(token) !== -1) { 10171 score += 70; 10172 } 10173 10174 if (parts.subtitle.indexOf(token) !== -1) { 10175 score += 45; 10176 } 10177 10178 if (parts.summary.indexOf(token) !== -1) { 10179 score += 35; 10180 } 10181 10182 if (parts.page.indexOf(token) !== -1) { 10183 score += 25; 10184 } 10185 10186 if (parts.content.indexOf(token) !== -1) { 10187 score += 12; 10188 } 10189 }); 10190 10191 if ( 10192 tokens.length > 1 && 10193 containsAllTokens(parts.title + " " + parts.keywords, tokens) 10194 ) { 10195 score += 120; 10196 } 10197 10198 if ( 10199 tokens.length > 1 && 10200 containsAllTokens(parts.summary + " " + parts.content, tokens) 10201 ) { 10202 score += 60; 10203 } 10204 10205 return score; 10206 } 10207 10208 function getBaseBoost(item) { 10209 var source = getDatasetValue(item, "searchSource"); 10210 var title = normalizeText(getDatasetValue(item, "searchTitle")); 10211 var subtitle = normalizeText(getDatasetValue(item, "searchSubtitle")); 10212 var keywords = normalizeText(getDatasetValue(item, "searchKeywords")); 10213 var text = title + " " + subtitle + " " + keywords; 10214 10215 if (source === "faq") { 10216 return 95; 10217 } 10218 10219 if ( 10220 text.indexOf("financiacion") !== -1 || 10221 text.indexOf("certificacion") !== -1 || 10222 text.indexOf("trabaja en enisa") !== -1 || 10223 text.indexOf("empleo") !== -1 10224 ) { 10225 return 100; 10226 } 10227 10228 if ( 10229 text.indexOf("requisitos") !== -1 || 10230 text.indexOf("condiciones") !== -1 || 10231 text.indexOf("obligaciones") !== -1 || 10232 text.indexOf("proceso") !== -1 10233 ) { 10234 return 90; 10235 } 10236 10237 if ( 10238 text.indexOf("contacto") !== -1 || 10239 text.indexOf("ayuda") !== -1 || 10240 text.indexOf("sistema interno") !== -1 || 10241 text.indexOf("canal etico") !== -1 10242 ) { 10243 return 85; 10244 } 10245 10246 if ( 10247 text.indexOf("privacidad") !== -1 || 10248 text.indexOf("cookies") !== -1 || 10249 text.indexOf("accesibilidad") !== -1 || 10250 text.indexOf("aviso legal") !== -1 10251 ) { 10252 return 65; 10253 } 10254 10255 return 55; 10256 } 10257 10258 function getItemScore(item, normalizedTerm, tokens) { 10259 var parts = getItemSearchParts(item); 10260 10261 if (!itemMatches(parts, normalizedTerm, tokens)) { 10262 return -1; 10263 } 10264 10265 return ( 10266 getBaseBoost(item) + 10267 scoreExactPhrase(parts, normalizedTerm) + 10268 scoreTokens(parts, tokens) 10269 ); 10270 } 10271 10272 function sortItems(items) { 10273 return items.sort(function (a, b) { 10274 var aScore = getNumberDatasetValue(a, "computedScore", 0); 10275 var bScore = getNumberDatasetValue(b, "computedScore", 0); 10276 10277 if (bScore !== aScore) { 10278 return bScore - aScore; 10279 } 10280 10281 var aOrder = getNumberDatasetValue(a, "searchOrder", 9999); 10282 var bOrder = getNumberDatasetValue(b, "searchOrder", 9999); 10283 10284 if (aOrder !== bOrder) { 10285 return aOrder - bOrder; 10286 } 10287 10288 return normalizeText( 10289 getDatasetValue(a, "searchTitle") 10290 ).localeCompare( 10291 normalizeText(getDatasetValue(b, "searchTitle")) 10292 ); 10293 }); 10294 } 10295 10296 function updateCounter(root, total, term, isReady, state) { 10297 var counter = root.querySelector(SELECTORS.counter); 10298 var message; 10299 10300 if (!counter) { 10301 return; 10302 } 10303 10304 if (!isReady) { 10305 counter.textContent = ""; 10306 counter.classList.add("is-loading"); 10307 counter.setAttribute("aria-busy", "true"); 10308 return; 10309 } 10310 10311 message = buildResultsMessage(total, term, state); 10312 counter.textContent = message; 10313 10314 counter.classList.remove("is-loading"); 10315 counter.setAttribute("aria-busy", "false"); 10316 } 10317 10318 function toggleEmptyState(root, hasResults, isReady) { 10319 var emptyResults = root.querySelector(SELECTORS.emptyResults); 10320 var results = root.querySelector(SELECTORS.results); 10321 10322 if (emptyResults) { 10323 emptyResults.hidden = !isReady || hasResults; 10324 } 10325 10326 if (!results) { 10327 return; 10328 } 10329 10330 if (!isReady) { 10331 results.hidden = true; 10332 results.classList.add("is-loading"); 10333 results.setAttribute("aria-busy", "true"); 10334 return; 10335 } 10336 10337 results.hidden = !hasResults; 10338 results.classList.remove("is-loading"); 10339 results.setAttribute("aria-busy", "false"); 10340 } 10341 10342 function filterAndRankItems(root, state) { 10343 var term = getSearchTerm(root);
10344 var normalizedTerm = normalizeText(term); 10345 var tokens = getSearchTokens(term); 10346 var matchedItems = []; 10347 10348 state.allItems.forEach(function (item) { 10349 var score = getItemScore(item, normalizedTerm, tokens); 10350 10351 if (score < 0) { 10352 item.dataset.computedScore = "0"; 10353 item.setAttribute("hidden", ""); 10354 item.classList.add(CLASSES.hiddenByPage); 10355 return; 10356 } 10357 10358 item.dataset.computedScore = String(score); 10359 matchedItems.push(item); 10360 }); 10361 10362 state.items = sortItems(matchedItems); 10363 state.currentPage = 1; 10364 10365 if (state.results) { 10366 state.items.forEach(function (item) { 10367 state.results.appendChild(item); 10368 }); 10369 } 10370 } 10371 10372 function buildAccentInsensitivePattern(term) { 10373 var value = String(term || ""); 10374 var pattern = ""; 10375 10376 for (var i = 0; i < value.length; i += 1) { 10377 var char = value.charAt(i); 10378 var normalized = normalizeText(char); 10379 10380 if (/\s/.test(char)) { 10381 pattern += "\\s+"; 10382 continue; 10383 } 10384 10385 if (ACCENT_GROUPS[normalized]) { 10386 pattern += "[" + escapeRegExp(ACCENT_GROUPS[normalized]) + "]"; 10387 continue; 10388 } 10389 10390 pattern += escapeRegExp(char); 10391 } 10392 10393 return pattern; 10394 } 10395 10396 function buildHighlightRegex(term) { 10397 var safeTerm = buildAccentInsensitivePattern(term); 10398 10399 if (/\s/.test(term)) { 10400 return new RegExp("(" + safeTerm + ")", "gi"); 10401 } 10402 10403 return new RegExp( 10404 "(^|[^A-Za-zÃ-ÃÃ-öø-ÿ0-9_\\-])([A-Za-zÃ-ÃÃ-öø-ÿ0-9_\\-]*" + 10405 safeTerm + 10406 "[A-Za-zÃ-ÃÃ-öø-ÿ0-9_\\-]*)", 10407 "gi" 10408 ); 10409 } 10410 10411 function clearHighlights(root) { 10412 var highlights = [].slice.call( 10413 root.querySelectorAll("." + CLASSES.highlight) 10414 ); 10415 10416 highlights.forEach(function (highlight) { 10417 var parent = highlight.parentNode; 10418 10419 if (!parent) { 10420 return; 10421 } 10422 10423 parent.replaceChild( 10424 document.createTextNode(highlight.textContent || ""), 10425 highlight 10426 ); 10427 10428 parent.normalize(); 10429 }); 10430 } 10431 10432 function highlightTextNode(node, term, regex) { 10433 var text = node.nodeValue; 10434 10435 if ( 10436 !text || 10437 normalizeText(text).indexOf(normalizeText(term)) === -1 10438 ) { 10439 return; 10440 } 10441 10442 var fragment = document.createDocumentFragment(); 10443 var lastIndex = 0; 10444 var match; 10445 10446 regex.lastIndex = 0; 10447 10448 while ((match = regex.exec(text)) !== null) { 10449 var prefix = match[1] || ""; 10450 var word = match[2] || ""; 10451 var matchStart = match.index; 10452 var wordStart = matchStart + prefix.length; 10453 10454 if (/\s/.test(term)) { 10455 prefix = ""; 10456 word = match[1] || ""; 10457 wordStart = matchStart; 10458 } 10459 10460 if (matchStart > lastIndex) { 10461 fragment.appendChild( 10462 document.createTextNode(text.slice(lastIndex, matchStart)) 10463 ); 10464 } 10465 10466 if (prefix) { 10467 fragment.appendChild(document.createTextNode(prefix)); 10468 } 10469 10470 if (word) { 10471 var mark = document.createElement("span"); 10472 10473 mark.className = CLASSES.highlight; 10474 mark.textContent = word; 10475 10476 fragment.appendChild(mark); 10477 } 10478 10479 lastIndex = wordStart + word.length; 10480 10481 if (regex.lastIndex === match.index) { 10482 regex.lastIndex += 1; 10483 } 10484 } 10485 10486 if (lastIndex === 0) { 10487 return; 10488 } 10489 10490 if (lastIndex < text.length) { 10491 fragment.appendChild( 10492 document.createTextNode(text.slice(lastIndex)) 10493 ); 10494 } 10495 10496 node.parentNode.replaceChild(fragment, node); 10497 } 10498 10499 function highlightElement(element, term, regex) { 10500 var walker = document.createTreeWalker( 10501 element, 10502 NodeFilter.SHOW_TEXT, 10503 { 10504 acceptNode: function (node) { 10505 if (!node.nodeValue || !node.nodeValue.trim()) { 10506 return NodeFilter.FILTER_REJECT; 10507 } 10508 10509 if ( 10510 node.parentElement && 10511 node.parentElement.closest("." + CLASSES.highlight) 10512 ) { 10513 return NodeFilter.FILTER_REJECT; 10514 } 10515 10516 return NodeFilter.FILTER_ACCEPT; 10517 } 10518 } 10519 ); 10520 10521 var nodes = []; 10522 var currentNode = walker.nextNode(); 10523 10524 while (currentNode) { 10525 nodes.push(currentNode); 10526 currentNode = walker.nextNode(); 10527 } 10528
10529 nodes.forEach(function (node) { 10530 highlightTextNode(node, term, regex); 10531 }); 10532 } 10533 10534 function applyHighlights(root, state) { 10535 var term = getSearchTerm(root); 10536 var tokens = getSearchTokens(term); 10537 var termsToHighlight = []; 10538 10539 clearHighlights(root); 10540 10541 if (normalizeSpaces(term).length >= MIN_HIGHLIGHT_LENGTH) { 10542 termsToHighlight.push(term); 10543 } 10544 10545 tokens.forEach(function (token) { 10546 if (termsToHighlight.indexOf(token) === -1) { 10547 termsToHighlight.push(token); 10548 } 10549 }); 10550 10551 termsToHighlight.forEach(function (highlightTerm) { 10552 var regex = buildHighlightRegex(highlightTerm); 10553 10554 var visibleItems = state.items.filter(function (item) { 10555 return !item.hasAttribute("hidden"); 10556 }); 10557 10558 visibleItems.forEach(function (item) { 10559 var targets = [].slice.call( 10560 item.querySelectorAll(SELECTORS.highlightTarget) 10561 ); 10562 10563 targets.forEach(function (target) { 10564 highlightElement(target, highlightTerm, regex); 10565 }); 10566 }); 10567 }); 10568 } 10569 10570 function createPaginationArrow(type, disabled, onClick) { 10571 var button = document.createElement("button"); 10572 10573 button.type = "button"; 10574 button.className = 10575 CLASSES.paginationButton + 10576 " " + 10577 CLASSES.paginationArrow + 10578 " " + 10579 (type === "prev" ? "is-prev" : "is-next"); 10580 10581 button.setAttribute( 10582 "aria-label", 10583 type === "prev" ? "Página anterior" : "Página siguiente" 10584 ); 10585 10586 button.innerHTML = 10587 '<span class="enisa-icon enisa-pagination__arrow-icon" aria-hidden="true"></span>'; 10588 10589 button.disabled = disabled; 10590 button.addEventListener("click", onClick); 10591 10592 return button; 10593 } 10594 10595 function renderPagination(root, state) { 10596 var totalPages = Math.max( 10597 1, 10598 Math.ceil(state.items.length / state.pageSize) 10599 ); 10600 10601 var pagination = state.pagination; 10602 10603 if (!pagination) { 10604 return; 10605 } 10606 10607 pagination.innerHTML = ""; 10608 10609 if (state.items.length === 0 || totalPages <= 1) { 10610 pagination.setAttribute("hidden", ""); 10611 return; 10612 } 10613 10614 pagination.removeAttribute("hidden"); 10615 10616 var half = Math.floor(PAGINATION_WINDOW_SIZE / 2); 10617 var start = Math.max(1, state.currentPage - half); 10618 var end = Math.min( 10619 totalPages, 10620 start + PAGINATION_WINDOW_SIZE - 1 10621 ); 10622 10623 if (end - start < PAGINATION_WINDOW_SIZE - 1) { 10624 start = Math.max( 10625 1, 10626 end - PAGINATION_WINDOW_SIZE + 1 10627 ); 10628 } 10629 10630 pagination.appendChild( 10631 createPaginationArrow( 10632 "prev", 10633 state.currentPage === 1, 10634 function () { 10635 if (state.currentPage <= 1) { 10636 return; 10637 } 10638 10639 state.currentPage -= 1; 10640 render(root, state, { 10641 type: "pagination", 10642 focusTarget: { type: "prev" } 10643 }); 10644 } 10645 ) 10646 ); 10647 10648 var pageList = document.createElement("div"); 10649 10650 pageList.className = CLASSES.paginationList; 10651 10652 for (var i = start; i <= end; i += 1) { 10653 var pageButton = document.createElement("button"); 10654 10655 pageButton.type = "button"; 10656 pageButton.className = 10657 CLASSES.paginationButton + 10658 (i === state.currentPage ? " " + CLASSES.active : ""); 10659 10660 pageButton.textContent = String(i); 10661 pageButton.setAttribute("aria-label", "Página " + i); 10662 10663 if (i === state.currentPage) { 10664 pageButton.setAttribute("aria-current", "page"); 10665 } 10666 10667 (function (pageNumber) { 10668 pageButton.addEventListener("click", function () { 10669 state.currentPage = pageNumber; 10670 render(root, state, { 10671 type: "pagination", 10672 focusTarget: { type: "page", page: pageNumber } 10673 }); 10674 }); 10675 })(i); 10676 10677 pageList.appendChild(pageButton); 10678 } 10679 10680 pagination.appendChild(pageList); 10681 10682 pagination.appendChild( 10683 createPaginationArrow( 10684 "next", 10685 state.currentPage === totalPages, 10686 function () { 10687 if (state.currentPage >= totalPages) { 10688 return; 10689 } 10690 10691 state.currentPage += 1; 10692 render(root, state, { 10693 type: "pagination", 10694 focusTarget: { type: "next" } 10695 }); 10696 } 10697 ) 10698 ); 10699 } 10700 10701 function renderItems(state) { 10702 var totalPages = Math.max( 10703 1,
10704 Math.ceil(state.items.length / state.pageSize) 10705 ); 10706 10707 if (state.currentPage > totalPages) { 10708 state.currentPage = totalPages; 10709 } 10710 10711 if (state.currentPage < 1) { 10712 state.currentPage = 1; 10713 } 10714 10715 var start = (state.currentPage - 1) * state.pageSize; 10716 var end = start + state.pageSize; 10717 10718 state.allItems.forEach(function (item) { 10719 item.setAttribute("hidden", ""); 10720 item.classList.add(CLASSES.hiddenByPage); 10721 }); 10722 10723 state.items.forEach(function (item, index) { 10724 var isVisible = index >= start && index < end; 10725 10726 item.classList.toggle( 10727 CLASSES.hiddenByPage, 10728 !isVisible 10729 ); 10730 10731 if (isVisible) { 10732 item.removeAttribute("hidden"); 10733 } else { 10734 item.setAttribute("hidden", ""); 10735 } 10736 }); 10737 } 10738 10739 function render(root, state, interaction) { 10740 var term = getSearchTerm(root); 10741 10742 renderItems(state); 10743 renderPagination(root, state); 10744 applyHighlights(root, state); 10745 toggleEmptyState( 10746 root, 10747 state.items.length > 0, 10748 true 10749 ); 10750 10751 updateCounter( 10752 root, 10753 state.items.length, 10754 term, 10755 true, 10756 state 10757 ); 10758 10759 if (interaction && interaction.type === "pagination") { 10760 restorePaginationFocus(root, interaction.focusTarget); 10761 announceResultsUpdate( 10762 root, 10763 state.items.length, 10764 term, 10765 state 10766 ); 10767 } 10768 } 10769 10770 function initGlobalSearch(root) { 10771 if ( 10772 !root || 10773 root.getAttribute("data-global-search-init") === "true" 10774 ) { 10775 return; 10776 } 10777 10778 root.setAttribute( 10779 "data-global-search-init", 10780 "true" 10781 ); 10782 10783 var shouldRestoreSearchFocus = 10784 consumeSearchFocusIntent() && 10785 getSearchTerm(root).length > 0; 10786 10787 bindForm(root); 10788 bindEntryPointerNavigation(root); 10789 10790 var allItems = [].slice.call( 10791 root.querySelectorAll(SELECTORS.item) 10792 ); 10793 10794 var pagination = root.querySelector( 10795 SELECTORS.pagination 10796 ); 10797 10798 var results = root.querySelector( 10799 SELECTORS.results 10800 ); 10801 10802 toggleEmptyState(root, false, false); 10803 updateCounter( 10804 root, 10805 0, 10806 getSearchTerm(root), 10807 false 10808 ); 10809 10810 if (!allItems.length || !results) { 10811 toggleEmptyState(root, false, true); 10812 10813 updateCounter( 10814 root, 10815 0, 10816 getSearchTerm(root), 10817 true 10818 ); 10819 10820 if (shouldRestoreSearchFocus) { 10821 focusResultsContext(root); 10822 } 10823 10824 return; 10825 } 10826 10827 var state = { 10828 currentPage: 1, 10829 pageSize: getPageSize(root), 10830 allItems: allItems, 10831 items: [], 10832 pagination: pagination, 10833 results: results 10834 }; 10835 10836 filterAndRankItems(root, state); 10837 render(root, state, null); 10838 10839 if (shouldRestoreSearchFocus) { 10840 focusResultsContext(root); 10841 } 10842 } 10843 10844 function init() { 10845 alignInitialHashTarget(); 10846 10847 var roots = [].slice.call( 10848 document.querySelectorAll(SELECTORS.root) 10849 ); 10850 10851 roots.forEach(initGlobalSearch); 10852 } 10853 10854 if (document.readyState === "loading") { 10855 document.addEventListener( 10856 "DOMContentLoaded", 10857 init 10858 ); 10859 10860 return; 10861 } 10862 10863 init(); 10864})(); 10865 10866/* 10867 ENISA | JS Module 10868 Modulo: 23-breadcrumb-back 10869 Descripcion: Navegacion "volver" inteligente para enlaces [data-back-smart]. 10870*/ 10871(function () { 10872 "use strict"; 10873 10874 var STORAGE_LAST_URL_KEY = "enisa:last-url"; 10875 var STORAGE_PREV_URL_KEY = "enisa:prev-url"; 10876 10877 function getCurrentRelativeUrl() { 10878 return window.location.pathname + window.location.search + window.location.hash; 10879 } 10880 10881 function safeStorageGet(key) { 10882 try { 10883 return window.sessionStorage.getItem(key) || ""; 10884 } catch (error) { 10885 return ""; 10886 } 10887 } 10888 10889 function safeStorageSet(key, value) { 10890 try { 10891 window.sessionStorage.setItem(key, value); 10892 } catch (error) { 10893 } 10894 } 10895 10896 function syncInternalNavigationState() { 10897 var currentUrl = getCurrentRelativeUrl(); 10898 var lastUrl = safeStorageGet(STORAGE_LAST_URL_KEY); 10899 10900 if (lastUrl && lastUrl !== currentUrl) { 10901 safeStorageSet(STORAGE_PREV_URL_KEY, lastUrl); 10902 } 10903 10904 safeStorageSet(STORAGE_LAST_URL_KEY, currentUrl); 10905 } 10906 10907 function shouldUseHistoryBack() { 10908 return window.history.length > 1; 10909 } 10910
10911 function getStoredPreviousUrl() { 10912 var previousUrl = safeStorageGet(STORAGE_PREV_URL_KEY); 10913 var currentUrl = getCurrentRelativeUrl(); 10914 if (!previousUrl || previousUrl === currentUrl) return ""; 10915 return previousUrl; 10916 } 10917 10918 function handleSmartBackClick(event) { 10919 if (shouldUseHistoryBack()) { 10920 event.preventDefault(); 10921 window.history.back(); 10922 return; 10923 } 10924 10925 var previousUrl = getStoredPreviousUrl(); 10926 if (!previousUrl) return; 10927 10928 event.preventDefault(); 10929 window.location.assign(previousUrl); 10930 } 10931 10932 function initSmartBackLinks() { 10933 var links = document.querySelectorAll("[data-back-smart]"); 10934 if (!links.length) return; 10935 10936 var i; 10937 var link; 10938 for (i = 0; i < links.length; i++) { 10939 link = links[i]; 10940 if (link.getAttribute("data-back-smart-init") === "true") continue; 10941 link.setAttribute("data-back-smart-init", "true"); 10942 link.addEventListener("click", handleSmartBackClick); 10943 } 10944 } 10945 10946 if (document.readyState === "loading") { 10947 document.addEventListener("DOMContentLoaded", function () { 10948 syncInternalNavigationState(); 10949 initSmartBackLinks(); 10950 }); 10951 } else { 10952 syncInternalNavigationState(); 10953 initSmartBackLinks(); 10954 } 10955})(); 10956 10957(function () { 10958 "use strict"; 10959 10960 var SELECTOR_SECTION = ".enisa-equipo-directivo"; 10961 var SELECTOR_TOGGLE = "[data-equipo-toggle]"; 10962 var SELECTOR_BIO = ".enisa-directivo__bio"; 10963 var INIT_ATTR = "data-equipo-init"; 10964 var LABEL_OPEN = "Ver trayectoria"; 10965 var LABEL_CLOSE = "Ocultar trayectoria"; 10966 var BLOCK_TAGS_REGEX = /<(p|div|ul|ol|li|table|thead|tbody|tr|td|th|blockquote|section|article|h[1-6])(\s|>)/i; 10967 10968 function normalizeBioParagraphs(section) { 10969 var bios = section.querySelectorAll(SELECTOR_BIO); 10970 for (var i = 0; i < bios.length; i++) { 10971 var bio = bios[i]; 10972 var html = (bio.innerHTML || "").trim(); 10973 if (!html) continue; 10974 if (bio.querySelector("p")) continue; 10975 if (BLOCK_TAGS_REGEX.test(html)) continue; 10976 10977 bio.innerHTML = "<p>" + html + "</p>"; 10978 } 10979 } 10980 10981 function initEquipo(section) { 10982 if (section.getAttribute(INIT_ATTR)) return; 10983 section.setAttribute(INIT_ATTR, "true"); 10984 normalizeBioParagraphs(section); 10985 10986 section.addEventListener("click", function (e) { 10987 var btn = e.target.closest(SELECTOR_TOGGLE); 10988 if (!btn) return; 10989 10990 var expanded = btn.getAttribute("aria-expanded") === "true"; 10991 var panelId = btn.getAttribute("aria-controls"); 10992 var panel = panelId ? document.getElementById(panelId) : null; 10993 var labelEl = btn.querySelector("[data-equipo-label]"); 10994 10995 if (!panel) return; 10996 10997 if (expanded) { 10998 btn.setAttribute("aria-expanded", "false"); 10999 panel.classList.remove("is-open"); 11000 if (labelEl) labelEl.textContent = LABEL_OPEN; 11001 } else { 11002 btn.setAttribute("aria-expanded", "true"); 11003 panel.classList.add("is-open"); 11004 if (labelEl) labelEl.textContent = LABEL_CLOSE; 11005 } 11006 }); 11007 } 11008 11009 function boot() { 11010 var sections = document.querySelectorAll(SELECTOR_SECTION); 11011 for (var i = 0; i < sections.length; i++) { 11012 initEquipo(sections[i]); 11013 } 11014 } 11015 11016 if (document.readyState === "loading") { 11017 document.addEventListener("DOMContentLoaded", boot); 11018 } else { 11019 boot(); 11020 } 11021})(); 11022 11023(function () { 11024 "use strict"; 11025 11026 var ROOT_SELECTOR = ".enisa-shareholders"; 11027 var NAME_SELECTOR = ".enisa-shareholders__name"; 11028 var LINE_CLASS = "enisa-shareholders__name-line"; 11029 var TOP_LINE_CLASS = "enisa-shareholders__name-line--top"; 11030 var BOTTOM_LINE_CLASS = "enisa-shareholders__name-line--bottom"; 11031 var ORIGINAL_ATTR = "data-enisa-shareholder-name-original"; 11032 var RESIZE_DEBOUNCE = 120; 11033 11034 function getOriginalText(name) { 11035 var original = name.getAttribute(ORIGINAL_ATTR); 11036 11037 if (original) { 11038 return original; 11039 } 11040 11041 original = (name.textContent || "").replace(/\s+/g, " ").trim(); 11042 name.setAttribute(ORIGINAL_ATTR, original); 11043 return original; 11044 } 11045 11046 function createMeasureNode(name) { 11047 var measure = document.createElement("span"); 11048 var styles = window.getComputedStyle(name); 11049 11050 measure.setAttribute("aria-hidden", "true"); 11051 measure.style.position = "absolute"; 11052 measure.style.visibility = "hidden"; 11053 measure.style.pointerEvents = "none"; 11054 measure.style.whiteSpace = "nowrap"; 11055 measure.style.left = "-9999px"; 11056 measure.style.top = "0"; 11057 measure.style.fontFamily = styles.fontFamily; 11058 measure.style.fontSize = styles.fontSize; 11059 measure.style.fontWeight = styles.fontWeight; 11060 measure.style.letterSpacing = styles.letterSpacing; 11061 measure.style.textTransform = styles.textTransform; 11062 11063 document.body.appendChild(measure); 11064 return measure; 11065 } 11066
11067 function getCssLength(name, variableName, fallbackValue) { 11068 var value = window.getComputedStyle(name).getPropertyValue(variableName).trim(); 11069 11070 if (!value) { 11071 return fallbackValue; 11072 } 11073 11074 var test = document.createElement("div"); 11075 test.style.position = "absolute"; 11076 test.style.visibility = "hidden"; 11077 test.style.width = value; 11078 name.appendChild(test); 11079 var width = test.getBoundingClientRect().width || fallbackValue; 11080 name.removeChild(test); 11081 return width; 11082 } 11083 11084 function measureTextWidth(measureNode, text) { 11085 measureNode.textContent = text; 11086 return measureNode.getBoundingClientRect().width; 11087 } 11088 11089 function pickSplit(words, topWidth, bottomWidth, measureNode) { 11090 var bestFit = null; 11091 var fallback = null; 11092 var index; 11093 11094 for (index = 1; index < words.length; index += 1) { 11095 var topText = words.slice(0, index).join(" "); 11096 var bottomText = words.slice(index).join(" "); 11097 var topTextWidth = measureTextWidth(measureNode, topText); 11098 var bottomTextWidth = measureTextWidth(measureNode, bottomText); 11099 var candidate = { 11100 top: topText, 11101 bottom: bottomText, 11102 topOverflow: Math.max(0, topTextWidth - topWidth), 11103 bottomOverflow: Math.max(0, bottomTextWidth - bottomWidth), 11104 bottomFill: bottomTextWidth / Math.max(bottomWidth, 1), 11105 }; 11106 11107 if (candidate.topOverflow === 0 && candidate.bottomOverflow === 0) { 11108 return candidate; 11109 } 11110 11111 if ( 11112 !fallback || 11113 candidate.bottomOverflow < fallback.bottomOverflow || 11114 ( 11115 candidate.bottomOverflow === fallback.bottomOverflow && 11116 candidate.topOverflow < fallback.topOverflow 11117 ) || 11118 ( 11119 candidate.bottomOverflow === fallback.bottomOverflow && 11120 candidate.topOverflow === fallback.topOverflow && 11121 candidate.bottomFill > fallback.bottomFill 11122 ) 11123 ) { 11124 fallback = candidate; 11125 } 11126 11127 if ( 11128 candidate.topOverflow === 0 && 11129 (!bestFit || candidate.bottomFill > bestFit.bottomFill) 11130 ) { 11131 bestFit = candidate; 11132 } 11133 } 11134 11135 return bestFit || fallback; 11136 } 11137
11138 function renderSplit(name, split) { 11139 if (!split || !split.top || !split.bottom) { 11140 return; 11141 } 11142 11143 var topLine = document.createElement("span"); 11144 var bottomLine = document.createElement("span"); 11145 11146 topLine.className = LINE_CLASS + " " + TOP_LINE_CLASS; 11147 bottomLine.className = LINE_CLASS + " " + BOTTOM_LINE_CLASS; 11148 topLine.textContent = split.top; 11149 bottomLine.textContent = split.bottom; 11150 11151 name.textContent = ""; 11152 name.appendChild(topLine); 11153 name.appendChild(bottomLine); 11154 } 11155 11156 function splitName(name) { 11157 if (!name || !document.body) { 11158 return; 11159 } 11160 11161 var originalText = getOriginalText(name); 11162 var words = originalText.split(" ").filter(Boolean); 11163 11164 if (words.length < 2) { 11165 name.textContent = originalText; 11166 return; 11167 } 11168 11169 name.textContent = originalText; 11170 11171 var totalWidth = name.getBoundingClientRect().width; 11172 if (!totalWidth) { 11173 return; 11174 } 11175 11176 var arrowWidth = getCssLength(name, "--enisa-link-arrow-w", 12); 11177 var gapWidth = getCssLength(name, "--enisa-shareholder-link-gap", 8); 11178 var topWidth = totalWidth; 11179 var bottomWidth = Math.max(0, totalWidth - arrowWidth - gapWidth); 11180 var measureNode = createMeasureNode(name); 11181 var split; 11182 11183 try { 11184 split = pickSplit(words, topWidth, bottomWidth, measureNode); 11185 } finally { 11186 document.body.removeChild(measureNode); 11187 } 11188 11189 if (!split) { 11190 name.textContent = originalText; 11191 return; 11192 } 11193 11194 renderSplit(name, split); 11195 } 11196 11197 function splitAllNames() { 11198 var names = document.querySelectorAll(ROOT_SELECTOR + " " + NAME_SELECTOR); 11199 11200 Array.prototype.forEach.call(names, splitName); 11201 } 11202 11203 function initResizeHandling() { 11204 var timer = null; 11205 11206 function rerender() { 11207 clearTimeout(timer); 11208 timer = setTimeout(splitAllNames, RESIZE_DEBOUNCE); 11209 } 11210 11211 window.addEventListener("resize", rerender); 11212 11213 if (document.fonts && document.fonts.ready) { 11214 document.fonts.ready.then(splitAllNames).catch(function () {}); 11215 } 11216 } 11217 11218 function init() { 11219 if (!document.querySelector(ROOT_SELECTOR)) { 11220 return; 11221 } 11222 11223 splitAllNames(); 11224 initResizeHandling(); 11225 } 11226 11227 if (document.readyState === "loading") { 11228 document.addEventListener("DOMContentLoaded", init); 11229 } else { 11230 init(); 11231 } 11232})(); 11233 11234/* 11235 ENISA | JS Module 11236 Modulo: 27-newsletter 11237 Descripcion: Gestiona la suscripcion al boletin y crea/reactiva registros en Dataverse mediante Power Pages Web API. 11238*/ 11239(function () { 11240 "use strict"; 11241 11242 var FORM_SELECTOR = "[data-newsletter-form]"; 11243 var INIT_ATTR = "data-newsletter-init"; 11244 var DEFAULT_WEBAPI_TABLE = "/_api/enisa_newslettersubscriptions"; 11245 var TOKEN_ENDPOINT = "/_layout/tokenhtml"; 11246 var RECAPTCHA_SCRIPT_ID = "enisa-google-recaptcha-api"; 11247 var RECAPTCHA_ONLOAD_CALLBACK = "enisaNewsletterRecaptchaReady"; 11248 var RECAPTCHA_ACTION = "newsletter_submit"; 11249 var formInstanceCount = 0; 11250 11251 var ID_FIELD = "enisa_newslettersubscriptionid"; 11252 var EMAIL_FIELD = "enisa_email"; 11253 var EMAIL_NORMALIZED_FIELD = "enisa_emailnormalized"; 11254 var PAGE_URL_FIELD = "enisa_pageurl"; 11255 var PRIVACY_FIELD = "enisa_privacyaccepted"; 11256 var SOURCE_FIELD = "enisa_source"; 11257 var SUBSCRIBED_ON_FIELD = "enisa_subscribedon"; 11258 var STATUS_FIELD = "enisa_subscriptionstatus"; 11259 11260 var STATUS_ACTIVE = 694380000; 11261 var STATUS_UNSUBSCRIBED = 694380001; 11262 11263 var MESSAGES = { 11264 required: "Introduce tu correo electrónico.", 11265 invalid: "Introduce un correo electrónico válido.", 11266 loading: "Estamos procesando tu suscripción...", 11267 success: "Tu suscripción se ha enviado correctamente. Revisa tu buzón de correo electrónico.", 11268 reactivated: "Tu suscripción se ha enviado correctamente. Revisa tu buzón de correo electrónico.", 11269 duplicated: "Este correo ya está suscrito al boletÃn.", 11270 error: "No hemos podido registrar tu suscripción. Inténtalo de nuevo.", 11271 recaptcha: "No se ha podido validar la protección anti-spam. Inténtalo de nuevo." 11272 }; 11273 11274 var SENT_SUBMIT_LABEL = "Enviado"; 11275 11276 function init() { 11277 var forms = [].slice.call(document.querySelectorAll(FORM_SELECTOR)); 11278 if (!forms.length) return; 11279
11280 forms.forEach(initNewsletterForm); 11281 } 11282 11283 function initNewsletterForm(form) { 11284 if (!form || form.getAttribute(INIT_ATTR) === "true") return; 11285 11286 var emailInput = getEmailInput(form); 11287 var submitBtn = getSubmitButton(form); 11288 var feedback = getFeedbackElement(form); 11289 11290 if (!emailInput || !submitBtn || !feedback) return; 11291 11292 form.setAttribute(INIT_ATTR, "true"); 11293 ensureScopedAccessibilityIds(form, emailInput, feedback); 11294 11295 if (!feedback.hasAttribute("aria-live")) { 11296 feedback.setAttribute("aria-live", "polite"); 11297 } 11298 11299 if (!feedback.hasAttribute("role")) { 11300 feedback.setAttribute("role", "status"); 11301 } 11302 11303 feedback.setAttribute("aria-atomic", "true"); 11304 storeBaseDescribedBy(emailInput, feedback); 11305 11306 submitBtn.setAttribute( 11307 "data-newsletter-default-label", 11308 getButtonLabel(submitBtn) 11309 ); 11310 11311 emailInput.addEventListener("input", function () { 11312 form.classList.remove("is-success", "is-duplicate", "is-submit-error", "is-error"); 11313 clearFieldError(emailInput, feedback); 11314 resetSubmitState(submitBtn); 11315 }); 11316 11317 emailInput.addEventListener("blur", function () { 11318 validateEmailField(emailInput, feedback, false); 11319 }); 11320 11321 form.addEventListener("submit", function (event) { 11322 event.preventDefault(); 11323 11324 form.classList.remove("is-success", "is-duplicate", "is-submit-error", "is-error"); 11325 resetSubmitState(submitBtn); 11326 11327 if (isHoneypotFilled(form)) { 11328 renderSuccess(form, emailInput, submitBtn, feedback, MESSAGES.success); 11329 return; 11330 } 11331 11332 if (!validateEmailField(emailInput, feedback, true)) { 11333 return; 11334 } 11335 11336 submitNewsletter(form, emailInput, submitBtn, feedback); 11337 }); 11338 } 11339 11340 function getEmailInput(form) { 11341 return form.querySelector("input[type='email'][name='email']"); 11342 } 11343 11344 function getSubmitButton(form) { 11345 return ( 11346 form.querySelector("[data-newsletter-submit]") || 11347 form.querySelector("button[type='submit']") 11348 ); 11349 } 11350 11351 function getFeedbackElement(form) { 11352 return ( 11353 form.querySelector("[data-newsletter-feedback]") || 11354 form.querySelector( 11355 ".enisa-newsletter__feedback, .enisa-newsletter-banner__feedback" 11356 ) 11357 ); 11358 } 11359 11360 function safeFocus(element) { 11361 if ( 11362 window.ENISA && 11363 window.ENISA.utils && 11364 typeof window.ENISA.utils.safeFocus === "function" 11365 ) { 11366 return window.ENISA.utils.safeFocus(element); 11367 } 11368 11369 if (!element || typeof element.focus !== "function") return false; 11370 11371 try { 11372 element.focus({ preventScroll: true }); 11373 } catch (error) { 11374 element.focus(); 11375 } 11376 11377 return true; 11378 } 11379 11380 function findLabelByFor(container, id) { 11381 if (!container || !id) return null; 11382 11383 var labels = container.querySelectorAll("label[for]"); 11384 11385 for (var i = 0; i < labels.length; i += 1) { 11386 if (labels[i].getAttribute("for") === id) { 11387 return labels[i]; 11388 } 11389 } 11390 11391 return null; 11392 } 11393 11394 function findFirstDescribedByTarget(container, input) { 11395 if (!container || !input) return null; 11396 11397 var ids = (input.getAttribute("aria-describedby") || "").split(/\s+/); 11398 11399 for (var i = 0; i < ids.length; i += 1) { 11400 if (!ids[i]) continue; 11401 11402 var candidate = container.querySelector("[id='" + ids[i] + "']"); 11403 11404 if (candidate) { 11405 return candidate; 11406 } 11407 } 11408 11409 return null; 11410 } 11411 11412 function buildScopedId(baseId, suffix) { 11413 var normalizedBase = String(baseId || "newsletter").replace( 11414 /[^A-Za-z0-9\-_:.]/g, 11415 "-" 11416 ); 11417 var normalizedSuffix = String(suffix || "instance").replace( 11418 /[^A-Za-z0-9\-_:.]/g, 11419 "-" 11420 ); 11421 11422 return normalizedBase + "-" + normalizedSuffix; 11423 } 11424 11425 function remapElementId(element, fallbackBase, suffix, idMap) { 11426 if (!element) return ""; 11427 11428 var currentId = (element.id || "").trim(); 11429 var nextId = buildScopedId(currentId || fallbackBase, suffix); 11430 11431 if (currentId) { 11432 idMap[currentId] = nextId; 11433 } 11434 11435 element.id = nextId; 11436 return nextId; 11437 } 11438 11439 function replaceIdReferences(value, idMap) { 11440 if (!value) return ""; 11441 11442 var ids = value.split(/\s+/); 11443 var resolved = []; 11444 11445 ids.forEach(function (id) { 11446 var nextId = idMap[id] || id; 11447 11448 if (nextId && resolved.indexOf(nextId) === -1) { 11449 resolved.push(nextId); 11450 } 11451 }); 11452 11453 return resolved.join(" "); 11454 } 11455 11456 function ensureScopedAccessibilityIds(form, emailInput, feedback) { 11457 if (!form || !emailInput || !feedback) return; 11458 11459 formInstanceCount += 1; 11460 11461 var suffix = getSource(form) + "-" + formInstanceCount; 11462 var idMap = {}; 11463 var section = form.closest(".enisa-newsletter-banner, .enisa-newsletter"); 11464 var title = 11465 section && 11466 section.querySelector( 11467 ".enisa-newsletter-banner__title, .enisa-newsletter__title" 11468 ); 11469 var legal = form.querySelector( 11470 ".enisa-newsletter-banner__legal, .enisa-newsletter__legal" 11471 ); 11472 var originalEmailId = emailInput.id; 11473 var emailLabel = findLabelByFor(form, originalEmailId); 11474 var emailHint = findFirstDescribedByTarget(form, emailInput); 11475 var honeypotInput = form.querySelector("[data-newsletter-honeypot]"); 11476 var honeypotLabel = honeypotInput 11477 ? findLabelByFor(form, honeypotInput.id) 11478 : null; 11479 11480 remapElementId(title, "newsletter-title", suffix, idMap); 11481 remapElementId(emailHint, "newsletter-email-hint", suffix, idMap); 11482 remapElementId(feedback, "newsletter-feedback", suffix, idMap); 11483 remapElementId(legal, "newsletter-legal", suffix, idMap); 11484 remapElementId(emailInput, "newsletter-email", suffix, idMap); 11485 remapElementId(honeypotInput, "newsletter-website", suffix, idMap); 11486 11487 if (section && section.hasAttribute("aria-labelledby")) { 11488 section.setAttribute( 11489 "aria-labelledby", 11490 replaceIdReferences(section.getAttribute("aria-labelledby"), idMap) 11491 ); 11492 } 11493 11494 if (emailLabel) { 11495 emailLabel.setAttribute("for", emailInput.id); 11496 } 11497 11498 if (honeypotLabel && honeypotInput) { 11499 honeypotLabel.setAttribute("for", honeypotInput.id); 11500 } 11501 11502 if (emailInput.hasAttribute("aria-describedby")) { 11503 emailInput.setAttribute( 11504 "aria-describedby", 11505 replaceIdReferences(emailInput.getAttribute("aria-describedby"), idMap) 11506 ); 11507 } 11508 11509 if (form.hasAttribute("aria-describedby")) { 11510 form.setAttribute( 11511 "aria-describedby", 11512 replaceIdReferences(form.getAttribute("aria-describedby"), idMap) 11513 ); 11514 } 11515 } 11516 11517 function isHoneypotFilled(form) { 11518 var honeypot = form.querySelector("[data-newsletter-honeypot]"); 11519 return !!(honeypot && honeypot.value && honeypot.value.trim().length > 0); 11520 } 11521 11522 function normalizeEmail(value) { 11523 return (value || "").replace(/\s+/g, "").trim().toLowerCase(); 11524 } 11525 11526 function isValidEmail(value) { 11527 var email = normalizeEmail(value); 11528 return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email); 11529 } 11530 11531 function escapeODataString(value) { 11532 return String(value || "").replace(/'/g, "''"); 11533 } 11534 11535 function getSource(form) { 11536 return ( 11537 form.getAttribute("data-source") || 11538 (form.closest(".enisa-newsletter-banner") && "newsletter-banner") || 11539 (form.closest(".enisa-newsletter") && "newsletter-default") || 11540 "newsletter" 11541 ); 11542 } 11543 11544 function getWebApiTable(form) { 11545 var endpoint = ( 11546 form.getAttribute("data-webapi-table") || DEFAULT_WEBAPI_TABLE 11547 ).trim(); 11548 11549 return endpoint || DEFAULT_WEBAPI_TABLE; 11550 } 11551 11552 function getCurrentPageUrl() { 11553 return window.location.href || ""; 11554 } 11555 11556 function getRecaptchaSiteKey(form) { 11557 return (form.getAttribute("data-recaptcha-site-key") || "").trim(); 11558 } 11559 11560 function getRecaptchaAction(form) { 11561 return (form.getAttribute("data-recaptcha-action") || RECAPTCHA_ACTION).trim(); 11562 } 11563 11564 function getRecaptchaTokenField(form) { 11565 return (form.getAttribute("data-recaptcha-token-field") || "").trim(); 11566 } 11567 11568 function hasRecaptchaEnabled(form) { 11569 return !!getRecaptchaSiteKey(form); 11570 } 11571 11572 function loadRecaptchaApi(siteKey) { 11573 return new Promise(function (resolve, reject) { 11574 var maxAttempts = 80; 11575 var attempt = 0; 11576 11577 function resolveWhenReady() { 11578 attempt += 1; 11579 11580 if ( 11581 window.grecaptcha && 11582 typeof window.grecaptcha.ready === "function" && 11583 typeof window.grecaptcha.execute === "function" 11584 ) { 11585 window.grecaptcha.ready(function () { 11586 resolve(window.grecaptcha); 11587 }); 11588 return; 11589 } 11590 11591 if (attempt >= maxAttempts) { 11592 reject(new Error("reCAPTCHA API unavailable")); 11593 return; 11594 } 11595 11596 window.setTimeout(resolveWhenReady, 100); 11597 } 11598 11599 if ( 11600 window.grecaptcha && 11601 typeof window.grecaptcha.ready === "function" && 11602 typeof window.grecaptcha.execute === "function" 11603 ) { 11604 window.grecaptcha.ready(function () { 11605 resolve(window.grecaptcha); 11606 }); 11607 return; 11608 } 11609 11610 var existingScript = document.getElementById(RECAPTCHA_SCRIPT_ID); 11611 11612 window[RECAPTCHA_ONLOAD_CALLBACK] = function () { 11613 resolveWhenReady(); 11614 }; 11615 11616 if (existingScript) { 11617 resolveWhenReady(); 11618 return; 11619 } 11620 11621 var script = document.createElement("script"); 11622 script.id = RECAPTCHA_SCRIPT_ID; 11623 script.src = 11624 "https://www.google.com/recaptcha/api.js?onload=" + 11625 encodeURIComponent(RECAPTCHA_ONLOAD_CALLBACK) + 11626 "&render=" + 11627 encodeURIComponent(siteKey) + 11628 "&hl=es"; 11629 script.async = true; 11630 script.defer = true; 11631 script.onerror = function () { 11632 reject(new Error("reCAPTCHA script load error")); 11633 }; 11634 11635 document.head.appendChild(script); 11636 }); 11637 } 11638 11639 function getRecaptchaToken(form) { 11640 var siteKey = getRecaptchaSiteKey(form); 11641 11642 if (!siteKey) { 11643 return Promise.resolve(""); 11644 } 11645 11646 return loadRecaptchaApi(siteKey).then(function (grecaptcha) { 11647 return grecaptcha.execute(siteKey, { 11648 action: getRecaptchaAction(form) 11649 }); 11650 }); 11651 } 11652 11653 function getButtonLabel(button) { 11654 if (!button) return ""; 11655 11656 var label = button.querySelector(".enisa-btn__label"); 11657 return (label ? label.textContent : button.textContent || "").trim(); 11658 } 11659 11660 function setButtonLabel(button, label) { 11661 if (!button || !label) return; 11662 11663 var labelNode = button.querySelector(".enisa-btn__label"); 11664 11665 if (labelNode) { 11666 labelNode.textContent = label; 11667 return; 11668 } 11669 11670 labelNode = document.createElement("span"); 11671 labelNode.className = "enisa-btn__label"; 11672 labelNode.textContent = label; 11673 11674 button.textContent = ""; 11675 button.appendChild(labelNode); 11676 } 11677 11678 function resetSubmitState(submitBtn) { 11679 if (!submitBtn) return; 11680 11681 submitBtn.classList.remove("is-loading", "is-loaded", "is-done", "is-busy"); 11682 11683 if ( 11684 window.ENISA && 11685 window.ENISA.utils && 11686 typeof window.ENISA.utils.resetButtonProgress === "function" 11687 ) { 11688 window.ENISA.utils.resetButtonProgress(submitBtn); 11689 } else { 11690 submitBtn.style.removeProperty("--enisa-btn-loader-overlay-w"); 11691 } 11692 11693 submitBtn.disabled = false;
11694 submitBtn.setAttribute("aria-busy", "false"); 11695 11696 var defaultLabel = submitBtn.getAttribute("data-newsletter-default-label"); 11697 11698 if (defaultLabel) { 11699 setButtonLabel(submitBtn, defaultLabel); 11700 } 11701 } 11702 11703 function setBusy(form, submitBtn, feedback, isBusy) { 11704 form.classList.toggle("is-loading", isBusy); 11705 11706 if (submitBtn) { 11707 submitBtn.disabled = isBusy; 11708 submitBtn.classList.toggle("is-busy", isBusy); 11709 submitBtn.classList.remove("is-loading", "is-loaded", "is-done"); 11710 submitBtn.setAttribute("aria-busy", isBusy ? "true" : "false"); 11711 11712 if (isBusy) { 11713 submitBtn.style.removeProperty("--enisa-btn-loader-overlay-w"); 11714 } else if ( 11715 window.ENISA && 11716 window.ENISA.utils && 11717 typeof window.ENISA.utils.resetButtonProgress === "function" 11718 ) { 11719 window.ENISA.utils.resetButtonProgress(submitBtn); 11720 } else { 11721 submitBtn.style.removeProperty("--enisa-btn-loader-overlay-w"); 11722 } 11723 } 11724 11725 if (isBusy) { 11726 setFeedback(feedback, MESSAGES.loading, "status"); 11727 } 11728 } 11729 11730 function completeSubmitProgress(submitBtn) { 11731 if (!submitBtn) return; 11732 11733 submitBtn.disabled = false; 11734 submitBtn.classList.remove("is-loading", "is-loaded", "is-done", "is-busy"); 11735 11736 if ( 11737 window.ENISA && 11738 window.ENISA.utils && 11739 typeof window.ENISA.utils.resetButtonProgress === "function" 11740 ) { 11741 window.ENISA.utils.resetButtonProgress(submitBtn); 11742 } else { 11743 submitBtn.style.removeProperty("--enisa-btn-loader-overlay-w"); 11744 } 11745 11746 setButtonLabel(submitBtn, SENT_SUBMIT_LABEL); 11747 submitBtn.setAttribute("aria-busy", "false"); 11748 } 11749 11750 function setFeedback(feedback, message, mode) { 11751 if (!feedback) return; 11752 11753 feedback.textContent = message || ""; 11754 feedback.hidden = !message; 11755 feedback.classList.remove("is-error", "is-success"); 11756 feedback.removeAttribute("role"); 11757 11758 if (mode === "error") { 11759 feedback.classList.add("is-error"); 11760 feedback.setAttribute("role", "alert"); 11761 return; 11762 } 11763 11764 if (mode === "status" || mode === "success") { 11765 feedback.setAttribute("role", "status"); 11766 } 11767 11768 if (mode === "success") { 11769 feedback.classList.add("is-success"); 11770 } 11771 } 11772 11773 function getFieldWrapper(input) { 11774 if (!input) return null; 11775 11776 return input.closest( 11777 ".enisa-newsletter__field, .enisa-newsletter-banner__form-row, .enisa-field-group" 11778 ); 11779 } 11780 11781 function storeBaseDescribedBy(input, feedback) { 11782 if (!input) return; 11783 11784 var feedbackId = feedback && feedback.id ? feedback.id : ""; 11785 var current = (input.getAttribute("aria-describedby") || "").split(/\s+/); 11786 var baseIds = current.filter(function (id) { 11787 return id && id !== feedbackId; 11788 }); 11789 11790 input.setAttribute("data-newsletter-base-describedby", baseIds.join(" ")); 11791 updateInputDescribedBy(input, feedback, false); 11792 } 11793 11794 function updateInputDescribedBy(input, feedback, includeFeedback) { 11795 if (!input) return; 11796 11797 var ids = []; 11798 var baseIds = ( 11799 input.getAttribute("data-newsletter-base-describedby") || "" 11800 ).split(/\s+/); 11801 11802 baseIds.forEach(function (id) { 11803 if (id && ids.indexOf(id) === -1) { 11804 ids.push(id); 11805 } 11806 }); 11807 11808 if ( 11809 includeFeedback && 11810 feedback && 11811 feedback.id && 11812 ids.indexOf(feedback.id) === -1 11813 ) { 11814 ids.push(feedback.id); 11815 } 11816 11817 if (ids.length) { 11818 input.setAttribute("aria-describedby", ids.join(" ")); 11819 return; 11820 } 11821 11822 input.removeAttribute("aria-describedby"); 11823 } 11824 11825 function setFieldError(input, feedback, message, shouldFocus) { 11826 input.setAttribute("aria-invalid", "true"); 11827 updateInputDescribedBy(input, feedback, true); 11828 11829 var form = input.closest(FORM_SELECTOR); 11830 11831 if (form) { 11832 form.classList.add("is-error"); 11833 } 11834 11835 var field = getFieldWrapper(input); 11836 11837 if (field) { 11838 field.classList.add("is-error"); 11839 } 11840 11841 setFeedback(feedback, message, "error"); 11842 11843 if (shouldFocus) { 11844 safeFocus(input); 11845 } 11846 } 11847 11848 function clearFieldError(input, feedback) { 11849 input.setAttribute("aria-invalid", "false"); 11850 updateInputDescribedBy(input, feedback, false); 11851 11852 var form = input.closest(FORM_SELECTOR); 11853 11854 if (form) { 11855 form.classList.remove("is-error"); 11856 } 11857 11858 var field = getFieldWrapper(input); 11859 11860 if (field) { 11861 field.classList.remove("is-error"); 11862 } 11863 11864 if (feedback && feedback.classList.contains("is-error")) { 11865 setFeedback(feedback, "", null); 11866 } 11867 } 11868 11869 function validateEmailField(input, feedback, shouldFocus) { 11870 var rawValue = input.value || ""; 11871 var email = normalizeEmail(rawValue); 11872 11873 if (!email) { 11874 setFieldError(input, feedback, MESSAGES.required, shouldFocus); 11875 return false;
11876 } 11877 11878 if (!isValidEmail(email)) { 11879 setFieldError(input, feedback, MESSAGES.invalid, shouldFocus); 11880 return false; 11881 } 11882 11883 input.value = email; 11884 clearFieldError(input, feedback); 11885 return true; 11886 } 11887 11888 function getRequestVerificationToken() { 11889 if ( 11890 window.shell && 11891 typeof window.shell.getTokenDeferred === "function" 11892 ) { 11893 return new Promise(function (resolve, reject) { 11894 window.shell 11895 .getTokenDeferred() 11896 .done(function (token) { 11897 if (!token) { 11898 reject(new Error("Empty Power Pages security token")); 11899 return; 11900 } 11901 11902 resolve(token); 11903 }) 11904 .fail(function () { 11905 reject(new Error("Unable to get Power Pages security token")); 11906 }); 11907 }); 11908 } 11909 11910 return fetch(TOKEN_ENDPOINT, { 11911 method: "GET", 11912 credentials: "same-origin", 11913 headers: { 11914 Accept: "text/html" 11915 } 11916 }) 11917 .then(function (response) { 11918 if (!response.ok) { 11919 throw createWebApiError( 11920 response.status, 11921 "Token request failed: " + response.status 11922 ); 11923 } 11924 11925 return response.text(); 11926 }) 11927 .then(function (html) { 11928 var wrapper = document.createElement("div"); 11929 wrapper.innerHTML = html; 11930 11931 var tokenInput = wrapper.querySelector( 11932 "input[name='__RequestVerificationToken']" 11933 ); 11934 var token = tokenInput ? tokenInput.value : ""; 11935 11936 if (!token) { 11937 throw new Error("Request verification token not found"); 11938 } 11939 11940 return token; 11941 }); 11942 } 11943 11944 function createWebApiError(status, responseText) { 11945 var error = new Error( 11946 "Newsletter Web API error " + status + ": " + responseText 11947 ); 11948 error.status = status; 11949 error.responseText = responseText || ""; 11950 return error; 11951 } 11952 11953 function requestWebApi(url, options, token) { 11954 var method = options && options.method ? options.method : "GET"; 11955 var payload = 11956 options && Object.prototype.hasOwnProperty.call(options, "payload") 11957 ? options.payload 11958 : null; 11959 11960 var headers = { 11961 Accept: "application/json", 11962 "OData-MaxVersion": "4.0", 11963 "OData-Version": "4.0", 11964 __RequestVerificationToken: token 11965 }; 11966 11967 if (method !== "GET") { 11968 headers["Content-Type"] = "application/json; charset=utf-8"; 11969 } 11970 11971 if (method === "PATCH") { 11972 headers["If-Match"] = "*"; 11973 } 11974 11975 return fetch(url, { 11976 method: method, 11977 credentials: "same-origin", 11978 headers: headers, 11979 body: payload ? JSON.stringify(payload) : undefined 11980 }).then(function (response) { 11981 if (response.ok) { 11982 if (response.status === 204) { 11983 return null; 11984 } 11985 11986 return response.text().then(function (text) { 11987 return text ? JSON.parse(text) : null; 11988 }); 11989 } 11990 11991 return response.text().then(function (text) { 11992 throw createWebApiError(response.status, text); 11993 }); 11994 }); 11995 } 11996 11997 function findExistingSubscription(endpoint, email, token) { 11998 var normalizedEmail = normalizeEmail(email); 11999 var filter = 12000 EMAIL_NORMALIZED_FIELD + " eq '" + escapeODataString(normalizedEmail) + "'"; 12001 var select = [ID_FIELD, EMAIL_NORMALIZED_FIELD, STATUS_FIELD].join(","); 12002 12003 var url = 12004 endpoint + 12005 "?$select=" + 12006 encodeURIComponent(select) + 12007 "&$filter=" + 12008 encodeURIComponent(filter) + 12009 "&$top=1"; 12010 12011 return requestWebApi(url, { method: "GET" }, token).then(function (response) { 12012 var records = response && response.value ? response.value : []; 12013 return records.length ? records[0] : null; 12014 }); 12015 } 12016 12017 function buildCreatePayload(form, email, recaptchaToken) { 12018 var payload = {}; 12019 12020 payload[EMAIL_FIELD] = email; 12021 payload[EMAIL_NORMALIZED_FIELD] = email; 12022 payload[PAGE_URL_FIELD] = getCurrentPageUrl(); 12023 payload[PRIVACY_FIELD] = true; 12024 payload[SOURCE_FIELD] = getSource(form); 12025 payload[SUBSCRIBED_ON_FIELD] = new Date().toISOString(); 12026 payload[STATUS_FIELD] = STATUS_ACTIVE; 12027 12028 var recaptchaTokenField = getRecaptchaTokenField(form); 12029 12030 if (recaptchaToken && recaptchaTokenField) { 12031 payload[recaptchaTokenField] = recaptchaToken; 12032 } 12033 12034 return payload; 12035 } 12036 12037 function buildReactivatePayload(form, recaptchaToken) { 12038 var payload = {}; 12039 12040 payload[PAGE_URL_FIELD] = getCurrentPageUrl(); 12041 payload[PRIVACY_FIELD] = true; 12042 payload[SOURCE_FIELD] = getSource(form); 12043 payload[SUBSCRIBED_ON_FIELD] = new Date().toISOString(); 12044 payload[STATUS_FIELD] = STATUS_ACTIVE; 12045 12046 var recaptchaTokenField = getRecaptchaTokenField(form); 12047 12048 if (recaptchaToken && recaptchaTokenField) { 12049 payload[recaptchaTokenField] = recaptchaToken; 12050 } 12051 12052 return payload; 12053 } 12054 12055 function createSubscription(endpoint, payload, token) { 12056 return requestWebApi( 12057 endpoint, 12058 { 12059 method: "POST", 12060 payload: payload 12061 }, 12062 token 12063 ); 12064 } 12065 12066 function updateSubscription(endpoint, recordId, payload, token) { 12067 return requestWebApi( 12068 endpoint + "(" + recordId + ")", 12069 { 12070 method: "PATCH", 12071 payload: payload 12072 }, 12073 token 12074 ); 12075 } 12076 12077 function isActiveSubscription(record) { 12078 return Number(record && record[STATUS_FIELD]) === STATUS_ACTIVE; 12079 } 12080 12081 function isUnsubscribedSubscription(record) { 12082 return Number(record && record[STATUS_FIELD]) === STATUS_UNSUBSCRIBED; 12083 } 12084 12085 function isDuplicateError(error) { 12086 if (!error) return false;
12087 12088 var status = Number(error.status); 12089 var text = String(error.responseText || error.message || "").toLowerCase(); 12090 12091 return ( 12092 status === 409 || 12093 status === 412 || 12094 text.indexOf("duplicate") !== -1 || 12095 text.indexOf("alternate key") !== -1 || 12096 text.indexOf("already exists") !== -1 || 12097 text.indexOf("ya existe") !== -1 12098 ); 12099 } 12100 12101 function submitNewsletter(form, emailInput, submitBtn, feedback) { 12102 var email = normalizeEmail(emailInput.value); 12103 var endpoint = getWebApiTable(form); 12104 12105 setBusy(form, submitBtn, feedback, true); 12106 12107 getRecaptchaToken(form) 12108 .then(function (recaptchaToken) { 12109 return getRequestVerificationToken().then(function (token) { 12110 return { 12111 recaptchaToken: recaptchaToken, 12112 token: token 12113 }; 12114 }); 12115 }) 12116 .then(function (context) { 12117 var recaptchaToken = context.recaptchaToken; 12118 var token = context.token; 12119 12120 return findExistingSubscription(endpoint, email, token).then(function ( 12121 existingRecord 12122 ) { 12123 if (existingRecord && isActiveSubscription(existingRecord)) { 12124 renderDuplicate(form, emailInput, submitBtn, feedback); 12125 return null; 12126 } 12127 12128 if (existingRecord && isUnsubscribedSubscription(existingRecord)) { 12129 return updateSubscription( 12130 endpoint, 12131 existingRecord[ID_FIELD], 12132 buildReactivatePayload(form, recaptchaToken), 12133 token 12134 ).then(function () { 12135 renderSuccess( 12136 form, 12137 emailInput, 12138 submitBtn, 12139 feedback, 12140 MESSAGES.reactivated 12141 ); 12142 }); 12143 } 12144 12145 if (existingRecord) { 12146 return updateSubscription( 12147 endpoint, 12148 existingRecord[ID_FIELD], 12149 buildReactivatePayload(form, recaptchaToken), 12150 token 12151 ).then(function () { 12152 renderSuccess( 12153 form, 12154 emailInput, 12155 submitBtn, 12156 feedback, 12157 MESSAGES.reactivated 12158 ); 12159 }); 12160 } 12161 12162 return createSubscription( 12163 endpoint, 12164 buildCreatePayload(form, email, recaptchaToken), 12165 token 12166 ) 12167 .then(function () { 12168 renderSuccess( 12169 form, 12170 emailInput, 12171 submitBtn, 12172 feedback, 12173 MESSAGES.success 12174 ); 12175 }) 12176 .catch(function (error) { 12177 if (isDuplicateError(error)) { 12178 renderDuplicate(form, emailInput, submitBtn, feedback); 12179 return null; 12180 } 12181 12182 throw error; 12183 }); 12184 }); 12185 }) 12186 .catch(function (error) { 12187 if (window.console) { 12188 window.console.error("[ENISA newsletter]", error); 12189 } 12190 12191 if (isDuplicateError(error)) { 12192 renderDuplicate(form, emailInput, submitBtn, feedback); 12193 return; 12194 } 12195 12196 if (hasRecaptchaEnabled(form)) { 12197 var errorMessage = String(error && error.message ? error.message : "").toLowerCase(); 12198 12199 if ( 12200 errorMessage.indexOf("recaptcha") !== -1 || 12201 errorMessage.indexOf("captcha") !== -1 12202 ) { 12203 renderSubmitError(form, emailInput, submitBtn, feedback, MESSAGES.recaptcha); 12204 return; 12205 } 12206 } 12207 12208 renderSubmitError(form, emailInput, submitBtn, feedback); 12209 }); 12210 } 12211
12212 function renderSuccess(form, emailInput, submitBtn, feedback, message) { 12213 setBusy(form, submitBtn, feedback, false); 12214 12215 form.classList.add("is-success"); 12216 form.classList.remove("is-duplicate", "is-submit-error", "is-error"); 12217 setFeedback(feedback, message || MESSAGES.success, "success"); 12218 12219 if (emailInput) { 12220 emailInput.value = ""; 12221 emailInput.setAttribute("aria-invalid", "false"); 12222 12223 var field = getFieldWrapper(emailInput); 12224 12225 if (field) { 12226 field.classList.remove("is-error"); 12227 } 12228 } 12229 12230 completeSubmitProgress(submitBtn); 12231 } 12232 12233 function renderDuplicate(form, emailInput, submitBtn, feedback) { 12234 setBusy(form, submitBtn, feedback, false); 12235 resetSubmitState(submitBtn); 12236 12237 form.classList.remove("is-success", "is-submit-error"); 12238 form.classList.add("is-duplicate"); 12239 setFieldError(emailInput, feedback, MESSAGES.duplicated, true); 12240 } 12241 12242 function renderSubmitError(form, emailInput, submitBtn, feedback, message) { 12243 setBusy(form, submitBtn, feedback, false); 12244 resetSubmitState(submitBtn); 12245 12246 form.classList.remove("is-success", "is-duplicate"); 12247 form.classList.add("is-submit-error"); 12248 setFieldError(emailInput, feedback, message || MESSAGES.error, true); 12249 } 12250 12251 window.ENISA = window.ENISA || {}; 12252 window.ENISA.newsletter = { 12253 init: init 12254 }; 12255 12256 if (document.readyState === "loading") { 12257 document.addEventListener("DOMContentLoaded", init); 12258 } else { 12259 init(); 12260 } 12261})(); 12262 12263/* 12264 ENISA | JS Module 12265 Modulo: 28-newsletter-unsubscribe 12266 Descripcion: Gestiona la baja del boletin y actualiza el estado funcional de la suscripcion en Dataverse mediante Power Pages Web API. 12267*/ 12268(function () { 12269 "use strict"; 12270 12271 var ROOT_SELECTOR = "[data-newsletter-unsubscribe]"; 12272 var INIT_ATTR = "data-newsletter-unsubscribe-init"; 12273 var DEFAULT_WEBAPI_TABLE = "/_api/enisa_newslettersubscriptions"; 12274 var TOKEN_ENDPOINT = "/_layout/tokenhtml"; 12275 12276 var ID_FIELD = "enisa_newslettersubscriptionid"; 12277 var EMAIL_NORMALIZED_FIELD = "enisa_emailnormalized"; 12278 var STATUS_FIELD = "enisa_subscriptionstatus"; 12279 var SOURCE_FIELD = "enisa_source"; 12280 var PAGE_URL_FIELD = "enisa_pageurl"; 12281 12282 var STATUS_ACTIVE = 694380000; 12283 var STATUS_UNSUBSCRIBED = 694380001; 12284 12285 var MESSAGES = { 12286 required: "Introduce tu correo electrónico.", 12287 invalid: "Introduce un correo electrónico válido.", 12288 loading: "Estamos procesando tu solicitud...", 12289 success: "La baja de tu suscripción se ha tramitado correctamente. Revisa tu buzón de correo electrónico.", 12290 notFound: "Lo sentimos, este correo no consta como suscrito a nuestro boletÃn.", 12291 alreadyUnsubscribed: "Este correo ya estaba dado de baja del boletÃn.", 12292 error: "No hemos podido procesar la solicitud en este momento. Inténtalo de nuevo más tarde." 12293 }; 12294 var SENT_SUBMIT_LABEL = "Enviado"; 12295 12296 function init() { 12297 var roots = [].slice.call(document.querySelectorAll(ROOT_SELECTOR)); 12298 if (!roots.length) return; 12299 12300 roots.forEach(initUnsubscribeComponent); 12301 } 12302 12303 function initUnsubscribeComponent(root) { 12304 if (!root || root.getAttribute(INIT_ATTR) === "true") return; 12305 12306 var form = getForm(root); 12307 var emailInput = getEmailInput(root); 12308 var submitBtn = getSubmitButton(root); 12309 var feedback = getFeedbackElement(root); 12310 12311 if (!form || !emailInput || !submitBtn || !feedback) return; 12312 12313 root.setAttribute(INIT_ATTR, "true"); 12314 12315 if (!feedback.hasAttribute("aria-live")) { 12316 feedback.setAttribute("aria-live", "polite"); 12317 } 12318 12319 if (!feedback.hasAttribute("role")) { 12320 feedback.setAttribute("role", "status"); 12321 } 12322 12323 feedback.setAttribute("aria-atomic", "true"); 12324 updateInputDescribedBy(emailInput, feedback, false); 12325 submitBtn.setAttribute("data-newsletter-unsubscribe-default-label", getButtonLabel(submitBtn)); 12326 12327 emailInput.addEventListener("input", function () { 12328 root.classList.remove("is-success", "is-not-found", "is-submit-error", "is-already-unsubscribed", "is-error"); 12329 clearFieldError(emailInput, feedback); 12330 resetSubmitState(submitBtn); 12331 }); 12332 12333 emailInput.addEventListener("blur", function () { 12334 validateEmailField(emailInput, feedback, false); 12335 }); 12336 12337 form.addEventListener("submit", function (event) { 12338 event.preventDefault(); 12339 12340 root.classList.remove("is-success", "is-not-found", "is-submit-error", "is-already-unsubscribed", "is-error"); 12341 resetSubmitState(submitBtn); 12342 12343 if (isHoneypotFilled(root)) { 12344 renderSuccess(root, form, emailInput, submitBtn, feedback, MESSAGES.success); 12345 return; 12346 } 12347 12348 if (!validateEmailField(emailInput, feedback, true)) { 12349 return; 12350 } 12351 12352 submitUnsubscribe(root, form, emailInput, submitBtn, feedback); 12353 }); 12354 } 12355 12356 function getForm(root) { 12357 return root.querySelector("[data-newsletter-unsubscribe-form]"); 12358 } 12359 12360 function getEmailInput(root) { 12361 return root.querySelector("[data-newsletter-unsubscribe-email]") || root.querySelector("input[type='email'][name='email']"); 12362 } 12363 12364 function getSubmitButton(root) { 12365 return root.querySelector("[data-newsletter-unsubscribe-submit]") || root.querySelector("button[type='submit']"); 12366 } 12367 12368 function getFeedbackElement(root) { 12369 return root.querySelector("[data-newsletter-unsubscribe-feedback]") || root.querySelector(".enisa-newsletter-unsubscribe__feedback"); 12370 } 12371 12372 function safeFocus(element) { 12373 if ( 12374 window.ENISA && 12375 window.ENISA.utils && 12376 typeof window.ENISA.utils.safeFocus === "function" 12377 ) { 12378 return window.ENISA.utils.safeFocus(element); 12379 } 12380 12381 if (!element || typeof element.focus !== "function") return false;
12382 12383 try { 12384 element.focus({ preventScroll: true }); 12385 } catch (error) { 12386 element.focus(); 12387 } 12388 12389 return true; 12390 } 12391 12392 function isHoneypotFilled(root) { 12393 var honeypot = root.querySelector("[data-newsletter-honeypot]"); 12394 return !!(honeypot && honeypot.value && honeypot.value.trim().length > 0); 12395 } 12396 12397 function normalizeEmail(value) { 12398 return (value || "").replace(/\s+/g, "").trim().toLowerCase(); 12399 } 12400 12401 function isValidEmail(value) { 12402 var email = normalizeEmail(value); 12403 return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email); 12404 } 12405 12406 function escapeODataString(value) { 12407 return String(value || "").replace(/'/g, "''"); 12408 } 12409 12410 function getWebApiTable(root) { 12411 var endpoint = (root.getAttribute("data-webapi-table") || DEFAULT_WEBAPI_TABLE).trim(); 12412 return endpoint || DEFAULT_WEBAPI_TABLE; 12413 } 12414 12415 function getSource(root) { 12416 return ( 12417 root.getAttribute("data-source") || 12418 "newsletter-unsubscribe" 12419 ); 12420 } 12421 12422 function getCurrentPageUrl() { 12423 return window.location.href || ""; 12424 } 12425 12426 function getStatusValue(root) { 12427 var rawValue = root.getAttribute("data-status-value") || String(STATUS_UNSUBSCRIBED); 12428 var normalizedValue = String(rawValue).trim().replace(/\./g, ""); 12429 12430 if (!/^-?\d+$/.test(normalizedValue)) { 12431 return STATUS_UNSUBSCRIBED; 12432 } 12433 12434 return Number(normalizedValue); 12435 } 12436 12437 function getButtonLabel(button) { 12438 if (!button) return ""; 12439 12440 var label = button.querySelector(".enisa-btn__label"); 12441 return (label ? label.textContent : button.textContent || "").trim(); 12442 } 12443 12444 function setButtonLabel(button, label) { 12445 if (!button || !label) return; 12446 12447 var labelNode = button.querySelector(".enisa-btn__label"); 12448 12449 if (labelNode) { 12450 labelNode.textContent = label; 12451 return; 12452 } 12453 12454 labelNode = document.createElement("span"); 12455 labelNode.className = "enisa-btn__label"; 12456 labelNode.textContent = label; 12457 12458 button.textContent = ""; 12459 button.appendChild(labelNode); 12460 } 12461 12462 function resetSubmitState(submitBtn) { 12463 if (!submitBtn) return; 12464 12465 submitBtn.classList.remove("is-loading", "is-loaded", "is-done", "is-busy"); 12466 12467 if (window.ENISA && window.ENISA.utils && typeof window.ENISA.utils.resetButtonProgress === "function") { 12468 window.ENISA.utils.resetButtonProgress(submitBtn); 12469 } else { 12470 submitBtn.style.removeProperty("--enisa-btn-loader-overlay-w"); 12471 } 12472 12473 submitBtn.disabled = false; 12474 submitBtn.setAttribute("aria-busy", "false"); 12475 12476 var defaultLabel = submitBtn.getAttribute("data-newsletter-unsubscribe-default-label"); 12477 if (defaultLabel) { 12478 setButtonLabel(submitBtn, defaultLabel); 12479 } 12480 } 12481 12482 function setBusy(root, submitBtn, feedback, isBusy) { 12483 root.classList.toggle("is-loading", isBusy); 12484 12485 if (submitBtn) { 12486 submitBtn.disabled = isBusy; 12487 submitBtn.classList.toggle("is-busy", isBusy); 12488 submitBtn.classList.remove("is-loading", "is-loaded", "is-done"); 12489 submitBtn.setAttribute("aria-busy", isBusy ? "true" : "false"); 12490 12491 if (isBusy) { 12492 submitBtn.style.removeProperty("--enisa-btn-loader-overlay-w"); 12493 } else { 12494 if (window.ENISA && window.ENISA.utils && typeof window.ENISA.utils.resetButtonProgress === "function") { 12495 window.ENISA.utils.resetButtonProgress(submitBtn); 12496 } else { 12497 submitBtn.style.removeProperty("--enisa-btn-loader-overlay-w"); 12498 } 12499 } 12500 } 12501 12502 if (isBusy) { 12503 setFeedback(feedback, MESSAGES.loading, "status"); 12504 } 12505 } 12506 12507 function completeSubmitProgress(submitBtn) { 12508 if (!submitBtn) return; 12509 12510 submitBtn.disabled = false; 12511 submitBtn.classList.remove("is-loading", "is-loaded", "is-done", "is-busy"); 12512 12513 if (window.ENISA && window.ENISA.utils && typeof window.ENISA.utils.resetButtonProgress === "function") { 12514 window.ENISA.utils.resetButtonProgress(submitBtn); 12515 } else { 12516 submitBtn.style.removeProperty("--enisa-btn-loader-overlay-w"); 12517 } 12518 12519 setButtonLabel(submitBtn, SENT_SUBMIT_LABEL); 12520 submitBtn.setAttribute("aria-busy", "false"); 12521 } 12522 12523 function setFeedback(feedback, message, mode) { 12524 if (!feedback) return; 12525 12526 feedback.textContent = message || ""; 12527 feedback.hidden = !message; 12528 feedback.classList.remove("is-error", "is-success"); 12529 feedback.removeAttribute("role"); 12530 12531 if (mode === "error") { 12532 feedback.classList.add("is-error"); 12533 feedback.setAttribute("role", "alert"); 12534 return; 12535 } 12536 12537 if (mode === "status" || mode === "success") { 12538 feedback.setAttribute("role", "status"); 12539 } 12540 12541 if (mode === "success") { 12542 feedback.classList.add("is-success"); 12543 } 12544 } 12545 12546 function getFieldWrapper(input) { 12547 if (!input) return null; 12548 return input.closest(".enisa-newsletter-unsubscribe__form-row, .enisa-field-group"); 12549 } 12550 12551 function updateInputDescribedBy(input, feedback, includeFeedback) { 12552 if (!input || !feedback || !feedback.id) return; 12553 12554 var ids = (input.getAttribute("aria-describedby") || "") 12555 .split(/\s+/) 12556 .filter(function (id) { 12557 return id && id !== feedback.id; 12558 }); 12559 12560 if (includeFeedback) { 12561 ids.push(feedback.id); 12562 } 12563 12564 if (ids.length) { 12565 input.setAttribute("aria-describedby", ids.join(" ")); 12566 return; 12567 } 12568 12569 input.removeAttribute("aria-describedby"); 12570 } 12571 12572 function setFieldError(input, feedback, message, shouldFocus) { 12573 input.setAttribute("aria-invalid", "true"); 12574 updateInputDescribedBy(input, feedback, true); 12575 12576 var root = input.closest(ROOT_SELECTOR); 12577 if (root) { 12578 root.classList.add("is-error"); 12579 } 12580 12581 var field = getFieldWrapper(input); 12582 if (field) { 12583 field.classList.add("is-error"); 12584 } 12585 12586 setFeedback(feedback, message, "error"); 12587 12588 if (shouldFocus) { 12589 safeFocus(input); 12590 } 12591 } 12592 12593 function clearFieldError(input, feedback) { 12594 input.setAttribute("aria-invalid", "false"); 12595 updateInputDescribedBy(input, feedback, false); 12596 12597 var root = input.closest(ROOT_SELECTOR); 12598 if (root) { 12599 root.classList.remove("is-error"); 12600 } 12601 12602 var field = getFieldWrapper(input); 12603 if (field) { 12604 field.classList.remove("is-error"); 12605 } 12606 12607 if (feedback && feedback.classList.contains("is-error")) { 12608 setFeedback(feedback, "", null); 12609 } 12610 } 12611 12612 function validateEmailField(input, feedback, shouldFocus) { 12613 var rawValue = input.value || ""; 12614 var email = normalizeEmail(rawValue); 12615 12616 if (!email) { 12617 setFieldError(input, feedback, MESSAGES.required, shouldFocus); 12618 return false;
12619 } 12620 12621 if (!isValidEmail(email)) { 12622 setFieldError(input, feedback, MESSAGES.invalid, shouldFocus); 12623 return false; 12624 } 12625 12626 input.value = email; 12627 clearFieldError(input, feedback); 12628 return true; 12629 } 12630 12631 function getRequestVerificationToken() { 12632 if (window.shell && typeof window.shell.getTokenDeferred === "function") { 12633 return new Promise(function (resolve, reject) { 12634 window.shell.getTokenDeferred() 12635 .done(function (token) { 12636 if (!token) { 12637 reject(new Error("Request verification token not found")); 12638 return; 12639 } 12640 12641 resolve(token); 12642 }) 12643 .fail(function () { 12644 reject(new Error("Unable to get Power Pages security token")); 12645 }); 12646 }); 12647 } 12648 12649 return fetch(TOKEN_ENDPOINT, { 12650 method: "GET", 12651 credentials: "same-origin", 12652 headers: { 12653 "Accept": "text/html" 12654 } 12655 }) 12656 .then(function (response) { 12657 if (!response.ok) { 12658 throw createWebApiError(response.status, "Token request failed: " + response.status); 12659 } 12660 12661 return response.text(); 12662 }) 12663 .then(function (html) { 12664 var wrapper = document.createElement("div"); 12665 wrapper.innerHTML = html; 12666 12667 var tokenInput = wrapper.querySelector("input[name='__RequestVerificationToken']"); 12668 var token = tokenInput ? tokenInput.value : ""; 12669 12670 if (!token) { 12671 throw new Error("Request verification token not found"); 12672 } 12673 12674 return token; 12675 }); 12676 } 12677 12678 function createWebApiError(status, responseText) { 12679 var error = new Error("Newsletter unsubscribe Web API error " + status + ": " + responseText); 12680 error.status = status; 12681 error.responseText = responseText || ""; 12682 return error; 12683 } 12684 12685 function requestWebApi(url, options, token) { 12686 var method = options && options.method ? options.method : "GET"; 12687 var payload = options && Object.prototype.hasOwnProperty.call(options, "payload") 12688 ? options.payload 12689 : null; 12690 12691 var headers = { 12692 "Accept": "application/json", 12693 "OData-MaxVersion": "4.0", 12694 "OData-Version": "4.0", 12695 "__RequestVerificationToken": token 12696 }; 12697 12698 if (method !== "GET") { 12699 headers["Content-Type"] = "application/json; charset=utf-8"; 12700 } 12701 12702 if (method === "PATCH") { 12703 headers["If-Match"] = "*"; 12704 } 12705 12706 return fetch(url, { 12707 method: method, 12708 credentials: "same-origin", 12709 headers: headers, 12710 body: payload ? JSON.stringify(payload) : undefined 12711 }) 12712 .then(function (response) { 12713 if (response.ok) { 12714 if (response.status === 204) { 12715 return null; 12716 } 12717 12718 return response.text().then(function (text) { 12719 return text ? JSON.parse(text) : null; 12720 }); 12721 } 12722 12723 return response.text().then(function (text) { 12724 throw createWebApiError(response.status, text); 12725 }); 12726 }); 12727 } 12728 12729 function findExistingSubscription(endpoint, email, token) { 12730 var normalizedEmail = normalizeEmail(email); 12731 var filter = EMAIL_NORMALIZED_FIELD + " eq '" + escapeODataString(normalizedEmail) + "'"; 12732 var select = [ 12733 ID_FIELD, 12734 EMAIL_NORMALIZED_FIELD, 12735 STATUS_FIELD 12736 ].join(","); 12737 12738 var url = endpoint 12739 + "?$select=" + encodeURIComponent(select) 12740 + "&$filter=" + encodeURIComponent(filter) 12741 + "&$top=1"; 12742 12743 return requestWebApi(url, { method: "GET" }, token) 12744 .then(function (response) { 12745 var records = response && response.value ? response.value : []; 12746 return records.length ? records[0] : null; 12747 }); 12748 } 12749 12750 function buildUnsubscribePayload(root) { 12751 var payload = {}; 12752 12753 payload[STATUS_FIELD] = getStatusValue(root); 12754 payload[SOURCE_FIELD] = getSource(root); 12755 payload[PAGE_URL_FIELD] = getCurrentPageUrl(); 12756 12757 return payload; 12758 } 12759 12760 function unsubscribeSubscription(root, endpoint, recordId, token) { 12761 return requestWebApi(endpoint + "(" + recordId + ")", { 12762 method: "PATCH", 12763 payload: buildUnsubscribePayload(root) 12764 }, token); 12765 } 12766 12767 function isUnsubscribed(record, root) { 12768 return Number(record && record[STATUS_FIELD]) === getStatusValue(root); 12769 } 12770 12771 function submitUnsubscribe(root, form, emailInput, submitBtn, feedback) { 12772 var email = normalizeEmail(emailInput.value); 12773 var endpoint = getWebApiTable(root); 12774 12775 setBusy(root, submitBtn, feedback, true); 12776 12777 getRequestVerificationToken() 12778 .then(function (token) { 12779 return findExistingSubscription(endpoint, email, token) 12780 .then(function (existingRecord) { 12781 if (!existingRecord) { 12782 renderNotFound(root, emailInput, submitBtn, feedback); 12783 return null; 12784 } 12785 12786 if (isUnsubscribed(existingRecord, root)) { 12787 renderAlreadyUnsubscribed(root, form, emailInput, submitBtn, feedback); 12788 return null; 12789 } 12790 12791 return unsubscribeSubscription( 12792 root, 12793 endpoint, 12794 existingRecord[ID_FIELD], 12795 token 12796 ).then(function () { 12797 renderSuccess(root, form, emailInput, submitBtn, feedback, MESSAGES.success); 12798 }); 12799 }); 12800 }) 12801 .catch(function (error) { 12802 if (window.console) { 12803 window.console.error("[ENISA newsletter unsubscribe]", error); 12804 } 12805 12806 renderSubmitError(root, emailInput, submitBtn, feedback); 12807 }); 12808 } 12809
12810 function renderSuccess(root, form, emailInput, submitBtn, feedback, message) { 12811 setBusy(root, submitBtn, feedback, false); 12812 12813 root.classList.add("is-success"); 12814 root.classList.remove("is-not-found", "is-submit-error", "is-already-unsubscribed", "is-error"); 12815 setFeedback(feedback, message || MESSAGES.success, "success"); 12816 12817 if (form && typeof form.reset === "function") { 12818 form.reset(); 12819 } 12820 12821 if (emailInput) { 12822 emailInput.setAttribute("aria-invalid", "false"); 12823 12824 var field = getFieldWrapper(emailInput); 12825 if (field) { 12826 field.classList.remove("is-error"); 12827 } 12828 } 12829 12830 completeSubmitProgress(submitBtn); 12831 } 12832 12833 function renderAlreadyUnsubscribed(root, form, emailInput, submitBtn, feedback) { 12834 setBusy(root, submitBtn, feedback, false); 12835 12836 root.classList.add("is-success", "is-already-unsubscribed"); 12837 root.classList.remove("is-not-found", "is-submit-error", "is-error"); 12838 setFeedback(feedback, MESSAGES.alreadyUnsubscribed, "success"); 12839 12840 if (form && typeof form.reset === "function") { 12841 form.reset(); 12842 } 12843 12844 if (emailInput) { 12845 emailInput.setAttribute("aria-invalid", "false"); 12846 12847 var field = getFieldWrapper(emailInput); 12848 if (field) { 12849 field.classList.remove("is-error"); 12850 } 12851 } 12852 12853 completeSubmitProgress(submitBtn); 12854 } 12855 12856 function renderNotFound(root, emailInput, submitBtn, feedback) { 12857 setBusy(root, submitBtn, feedback, false); 12858 resetSubmitState(submitBtn); 12859 12860 root.classList.remove("is-success", "is-submit-error", "is-already-unsubscribed"); 12861 root.classList.add("is-not-found"); 12862 setFieldError(emailInput, feedback, MESSAGES.notFound, true); 12863 } 12864
12865 function renderSubmitError(root, emailInput, submitBtn, feedback) { 12866 setBusy(root, submitBtn, feedback, false); 12867 resetSubmitState(submitBtn); 12868 12869 root.classList.remove("is-success", "is-not-found", "is-already-unsubscribed"); 12870 root.classList.add("is-submit-error"); 12871 setFieldError(emailInput, feedback, MESSAGES.error, true); 12872 } 12873 12874 window.ENISA = window.ENISA || {}; 12875 window.ENISA.newsletterUnsubscribe = { 12876 init: init 12877 }; 12878 12879 if (document.readyState === "loading") { 12880 document.addEventListener("DOMContentLoaded", init); 12881 } else { 12882 init(); 12883 } 12884})(); 12885 12886/* 12887 ENISA | JS Module 12888 Modulo: 29-form-contact 12889 Descripcion: Gestion del formulario de contacto con Power Pages Web API, adjuntos como notas y validacion accesible. 12890*/ 12891(function () { 12892 "use strict"; 12893 12894 var ROOT_SELECTOR = "[data-cmp-contacto]"; 12895 var INIT_ATTR = "data-contacto-init"; 12896 var RECAPTCHA_SCRIPT_ID = "enisa-google-recaptcha-api"; 12897 var RECAPTCHA_READY_EVENT = "enisa:recaptcha-ready"; 12898 12899 var PRIMARY_NAME_FIELD = "enisa_name"; 12900 12901 var CONTACT_FIELDS = { 12902 name: PRIMARY_NAME_FIELD, 12903 lastname: "enisa_lastname", 12904 email: "enisa_email", 12905 departmentKey: "enisa_departmentkey", 12906 departmentLabel: "enisa_departmentlabel", 12907 message: "enisa_message", 12908 privacyAccepted: "enisa_privacyaccepted", 12909 targetEmail: "enisa_targetemail", 12910 }; 12911 12912 var DEFAULT_ERROR_MESSAGE = 12913 "No se ha podido enviar el formulario. Por favor, inténtalo de nuevo."; 12914 var DEFAULT_SUCCESS_MESSAGE = 12915 "Tu consulta se ha enviado correctamente. Revisa tu buzón de correo electrónico."; 12916 var DEFAULT_SUBMIT_LABEL = "Enviar consulta"; 12917 var SUBMITTING_SUBMIT_LABEL = "Enviando"; 12918 var SENT_SUBMIT_LABEL = "Enviado"; 12919 var MAX_FILE_SIZE_FALLBACK = 10485760; 12920 12921 function getButtonLabel(button) { 12922 if (!button) return ""; 12923 12924 var label = button.querySelector(".enisa-btn__label"); 12925 return (label ? label.textContent : button.textContent || "").trim(); 12926 } 12927 12928 function setButtonLabel(button, label) { 12929 if (!button || !label) return; 12930 12931 var labelNode = button.querySelector(".enisa-btn__label"); 12932 12933 if (labelNode) { 12934 labelNode.textContent = label; 12935 return; 12936 } 12937 12938 labelNode = document.createElement("span"); 12939 labelNode.className = "enisa-btn__label"; 12940 labelNode.textContent = label; 12941 12942 button.textContent = ""; 12943 button.appendChild(labelNode); 12944 } 12945 12946 function clearSubmitProgressTimer(button) { 12947 if (!button || !button.__enisaContactProgressTimer) return; 12948 12949 window.clearInterval(button.__enisaContactProgressTimer); 12950 button.__enisaContactProgressTimer = null; 12951 } 12952 12953 function resetSubmitProgress(button) { 12954 if (!button) return; 12955 12956 clearSubmitProgressTimer(button); 12957 button.style.removeProperty("--enisa-btn-loader-overlay-w"); 12958 } 12959 12960 function startSubmitProgress(button) { 12961 var current = 8; 12962 var target = 86; 12963 12964 if (!button) return; 12965 12966 resetSubmitProgress(button); 12967 button.style.setProperty("--enisa-btn-loader-overlay-w", current + "%"); 12968 12969 button.__enisaContactProgressTimer = window.setInterval(function () { 12970 current = Math.min( 12971 target, 12972 current + Math.max((target - current) * 0.18, 1.4), 12973 ); 12974 12975 button.style.setProperty( 12976 "--enisa-btn-loader-overlay-w", 12977 current.toFixed(2) + "%", 12978 ); 12979 12980 if (current >= target) { 12981 clearSubmitProgressTimer(button); 12982 } 12983 }, 160); 12984 } 12985 12986 function completeSubmitProgress(button) { 12987 if (!button) return; 12988 12989 clearSubmitProgressTimer(button); 12990 button.style.setProperty("--enisa-btn-loader-overlay-w", "100%"); 12991 } 12992 12993 function setSubmitButtonState(button, state) { 12994 if (!button) return; 12995 12996 if (!button.hasAttribute("data-contact-submit-default-label")) { 12997 button.setAttribute( 12998 "data-contact-submit-default-label", 12999 getButtonLabel(button) || DEFAULT_SUBMIT_LABEL, 13000 ); 13001 } 13002 13003 button.classList.add("enisa-btn--progress"); 13004 13005 if (state === "submitting") { 13006 button.disabled = true; 13007 button.classList.add("is-loading"); 13008 button.classList.remove("is-loaded", "is-done"); 13009 button.setAttribute("aria-busy", "true"); 13010 setButtonLabel(button, SUBMITTING_SUBMIT_LABEL); 13011 startSubmitProgress(button); 13012 return; 13013 } 13014 13015 if (state === "success") { 13016 button.disabled = true; 13017 button.classList.remove("is-loading"); 13018 button.classList.add("is-loaded", "is-done"); 13019 button.setAttribute("aria-busy", "false");
13020 setButtonLabel(button, SENT_SUBMIT_LABEL); 13021 completeSubmitProgress(button); 13022 return; 13023 } 13024 13025 button.disabled = false; 13026 button.classList.remove("is-loading", "is-loaded", "is-done"); 13027 button.setAttribute("aria-busy", "false"); 13028 13029 setButtonLabel( 13030 button, 13031 button.getAttribute("data-contact-submit-default-label") || 13032 DEFAULT_SUBMIT_LABEL, 13033 ); 13034 13035 resetSubmitProgress(button); 13036 } 13037 13038 function dispatchRecaptchaReady() { 13039 document.dispatchEvent(new CustomEvent(RECAPTCHA_READY_EVENT)); 13040 } 13041 13042 function loadRecaptchaScript() { 13043 if ( 13044 window.grecaptcha && 13045 typeof window.grecaptcha.render === "function" 13046 ) { 13047 dispatchRecaptchaReady(); 13048 return; 13049 } 13050 13051 if (document.getElementById(RECAPTCHA_SCRIPT_ID)) return; 13052 13053 window.enisaRecaptchaOnload = function () { 13054 dispatchRecaptchaReady(); 13055 }; 13056 13057 var script = document.createElement("script"); 13058 script.id = RECAPTCHA_SCRIPT_ID; 13059 script.src = 13060 "https://www.google.com/recaptcha/api.js?onload=enisaRecaptchaOnload&render=explicit&hl=es"; 13061 script.async = true; 13062 script.defer = true; 13063 13064 document.head.appendChild(script); 13065 } 13066 13067 function ready(callback) { 13068 if (document.readyState === "loading") { 13069 document.addEventListener("DOMContentLoaded", callback); 13070 return; 13071 } 13072 13073 callback(); 13074 } 13075 13076 function debounce(fn, delay) { 13077 var timer = null; 13078 13079 return function () { 13080 var args = arguments; 13081 var ctx = this; 13082 13083 window.clearTimeout(timer); 13084 13085 timer = window.setTimeout(function () { 13086 fn.apply(ctx, args); 13087 }, delay || 150); 13088 }; 13089 } 13090 13091 function safeFocus(element) { 13092 if ( 13093 window.ENISA && 13094 window.ENISA.utils && 13095 typeof window.ENISA.utils.safeFocus === "function" 13096 ) { 13097 return window.ENISA.utils.safeFocus(element); 13098 } 13099 13100 if (!element || typeof element.focus !== "function") return false; 13101 13102 try { 13103 element.focus({ preventScroll: true }); 13104 } catch (error) { 13105 element.focus(); 13106 } 13107 13108 return true; 13109 } 13110 13111 function trimValue(value) { 13112 return String(value || "") 13113 .replace(/\s+/g, " ") 13114 .trim(); 13115 } 13116 13117 function normalizeEmail(value) { 13118 return trimValue(value).toLowerCase(); 13119 } 13120 13121 function isValidEmail(value) { 13122 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizeEmail(value)); 13123 } 13124 13125 function decodeHtmlEntities(value) { 13126 if (!value || value.indexOf("&") === -1) return value || ""; 13127 13128 var textarea = document.createElement("textarea"); 13129 textarea.innerHTML = value; 13130 13131 return textarea.value; 13132 } 13133 13134 function parseJsonAttribute(value, fallback) { 13135 var raw = decodeHtmlEntities(value || "").trim(); 13136 13137 if (!raw) return fallback; 13138 13139 try { 13140 return JSON.parse(raw); 13141 } catch (error) { 13142 return fallback; 13143 } 13144 } 13145 13146 function getSelectedOption(select) { 13147 if (!select || select.selectedIndex < 0) return null; 13148 13149 return select.options[select.selectedIndex] || null; 13150 } 13151 13152 function getSelectedOptionLabel(select) { 13153 var option = getSelectedOption(select); 13154 13155 return option 13156 ? trimValue(option.textContent || option.label || "") 13157 : ""; 13158 } 13159 13160 function getSelectedOptionValue(select) { 13161 var option = getSelectedOption(select); 13162 13163 return option ? trimValue(option.value || "") : ""; 13164 } 13165 13166 function getAllowedExtensions(input) { 13167 var raw = input 13168 ? input.getAttribute("data-allowed-extensions") || "" 13169 : ""; 13170 13171 return raw 13172 .split(",") 13173 .map(function (item) { 13174 return trimValue(item) 13175 .replace(/^\./, "") 13176 .toLowerCase(); 13177 }) 13178 .filter(Boolean); 13179 } 13180 13181 function getFileExtension(fileName) { 13182 var parts = String(fileName || "").split("."); 13183 13184 if (parts.length < 2) return ""; 13185 13186 return parts.pop().toLowerCase(); 13187 } 13188 13189 function getMaxFileSize(input) { 13190 var value = input 13191 ? parseInt(input.getAttribute("data-max-size"), 10) 13192 : NaN; 13193 13194 return Number.isFinite(value) && value > 0 13195 ? value 13196 : MAX_FILE_SIZE_FALLBACK; 13197 } 13198 13199 function bytesToMb(bytes) { 13200 return Math.round((bytes / 1024 / 1024) * 10) / 10; 13201 } 13202 13203 function getPortalToken() { 13204 return new Promise(function (resolve, reject) { 13205 if ( 13206 !window.shell || 13207 typeof window.shell.getTokenDeferred !== "function" 13208 ) { 13209 reject( 13210 new Error("Power Pages token provider no disponible."), 13211 ); 13212 return; 13213 } 13214 13215 window.shell 13216 .getTokenDeferred() 13217 .done(function (token) { 13218 resolve(token); 13219 }) 13220 .fail(function () { 13221 reject( 13222 new Error( 13223 "No se ha podido obtener el token de seguridad.", 13224 ), 13225 ); 13226 }); 13227 }); 13228 } 13229 13230 function validateLoginSession(data, textStatus, xhr, resolve) { 13231 if (typeof window.validateLoginSession === "function") { 13232 window.validateLoginSession( 13233 data, 13234 textStatus, 13235 xhr, 13236 resolve, 13237 ); 13238 13239 return; 13240 } 13241 13242 resolve(data, textStatus, xhr); 13243 } 13244 13245 function webApiRequest(options) { 13246 return getPortalToken().then(function (token) { 13247 var headers = { 13248 Accept: "application/json", 13249 "Content-Type": "application/json; charset=utf-8", 13250 __RequestVerificationToken: token, 13251 }; 13252 13253 Object.keys(options.headers || {}
13253).forEach(function (key) { 13254 headers[key] = options.headers[key]; 13255 }); 13256 13257 if ( 13258 window.jQuery && 13259 typeof window.jQuery.ajax === "function" 13260 ) { 13261 return new Promise(function (resolve, reject) { 13262 window.jQuery 13263 .ajax({ 13264 type: options.method || "GET", 13265 url: options.url, 13266 data: options.body || null, 13267 headers: headers, 13268 contentType: headers["Content-Type"], 13269 }) 13270 .done(function (data, textStatus, xhr) { 13271 validateLoginSession( 13272 data, 13273 textStatus, 13274 xhr, 13275 function () { 13276 resolve({ 13277 data: data, 13278 textStatus: textStatus, 13279 xhr: xhr, 13280 getResponseHeader: function (name) { 13281 return xhr && 13282 typeof xhr.getResponseHeader === 13283 "function" 13284 ? xhr.getResponseHeader(name) 13285 : null; 13286 }, 13287 }); 13288 }, 13289 ); 13290 }) 13291 .fail(function (xhr) { 13292 reject(createWebApiError(xhr)); 13293 }); 13294 }); 13295 } 13296 13297 return window 13298 .fetch(options.url, { 13299 method: options.method || "GET", 13300 body: options.body || null, 13301 headers: headers, 13302 credentials: "same-origin", 13303 }) 13304 .then(function (response) { 13305 if (!response.ok) { 13306 return response.text().then(function (text) { 13307 throw createFetchWebApiError(response, text); 13308 }); 13309 } 13310 13311 return { 13312 response: response, 13313 getResponseHeader: function (name) { 13314 return response.headers.get(name); 13315 }, 13316 }; 13317 }); 13318 }); 13319 } 13320 13321 function createWebApiError(xhr) { 13322 var message = DEFAULT_ERROR_MESSAGE; 13323 var detail = ""; 13324 13325 if ( 13326 xhr && 13327 xhr.responseJSON && 13328 xhr.responseJSON.error 13329 ) { 13330 detail = xhr.responseJSON.error.message || ""; 13331 } else if (xhr && xhr.responseText) { 13332 detail = xhr.responseText; 13333 } 13334 13335 if (detail) { 13336 message = detail; 13337 } 13338 13339 var error = new Error(message); 13340 error.xhr = xhr; 13341 13342 return error; 13343 } 13344 13345 function createFetchWebApiError(response, text) { 13346 var message = DEFAULT_ERROR_MESSAGE; 13347 13348 if (text) { 13349 try { 13350 var parsed = JSON.parse(text); 13351 13352 if ( 13353 parsed && 13354 parsed.error && 13355 parsed.error.message 13356 ) { 13357 message = parsed.error.message; 13358 } 13359 } catch (error) { 13360 message = text; 13361 } 13362 } 13363 13364 var requestError = new Error(message); 13365 requestError.response = response; 13366 requestError.responseText = text; 13367 13368 return requestError; 13369 } 13370 13371 function extractGuidFromEntityHeader(headerValue) { 13372 var value = String(headerValue || ""); 13373 var match = value.match(/\(([0-9a-fA-F-]{36})\)/); 13374 13375 if (match && match[1]) { 13376 return match[1]; 13377 } 13378 13379 match = value.match( 13380 /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/, 13381 ); 13382 13383 return match ? match[0] : ""; 13384 } 13385 13386 function readFileAsBase64(file) { 13387 return new Promise(function (resolve, reject) { 13388 var reader = new FileReader(); 13389 13390 reader.onload = function () { 13391 var result = String(reader.result || ""); 13392 var commaIndex = result.indexOf(","); 13393 13394 resolve( 13395 commaIndex >= 0 13396 ? result.substring(commaIndex + 1) 13397 : result, 13398 ); 13399 }; 13400 13401 reader.onerror = function () { 13402 reject( 13403 new Error( 13404 "No se ha podido leer el archivo " + 13405 file.name + 13406 ".", 13407 ), 13408 ); 13409 }; 13410 13411 reader.readAsDataURL(file); 13412 }); 13413 } 13414 13415 function ContactFormController(root) { 13416 this.root = root; 13417 this.form = root.querySelector("[data-contacto-form]"); 13418 this.fields = {}; 13419 this.isSubmitting = false; 13420 this.recaptchaWidgetId = null; 13421 this.departmentEmailMap = {}; 13422 } 13423 13424 ContactFormController.prototype.init = function () { 13425 if (!this.form) return; 13426 13427 this.cacheFields(); 13428 13429 this.departmentEmailMap = parseJsonAttribute( 13430 this.form.getAttribute("data-department-email-map"), 13431 {}, 13432 ); 13433 13434 this.initCustomSelects(); 13435 this.initRecaptcha(); 13436 this.bindEvents(); 13437 this.updateFileList(); 13438 }; 13439 13440 ContactFormController.prototype.cacheFields = function () { 13441 var self = this; 13442 13443 this.form 13444 .querySelectorAll("[data-field]") 13445 .forEach(function (field) { 13446 var fieldName = field.getAttribute("data-field"); 13447 13448 if (!fieldName) return; 13449 13450 self.fields[fieldName] = field; 13451 }); 13452 13453 this.submitBtn = this.form.querySelector( 13454 "[data-submit-btn]", 13455 ); 13456 13457 setSubmitButtonState(this.submitBtn, "idle"); 13458 13459 this.loadingMsg = this.form.querySelector( 13460 "[data-loading]", 13461 ); 13462 13463 this.successMsg = this.form.querySelector( 13464 "[data-success]", 13465 ); 13466 13467 this.errorSummary = this.form.querySelector( 13468 "[data-error-summary], [data-error-form]", 13469 ); 13470 13471 if (this.errorSummary) { 13472 this.errorSummary.setAttribute("role", "alert"); 13473 this.errorSummary.setAttribute("tabindex", "-1"); 13474 } 13475 13476 if (this.successMsg) { 13477 this.successMsg.setAttribute("role", "status"); 13478 this.successMsg.setAttribute( 13479 "aria-live", 13480 "polite", 13481 ); 13482 this.successMsg.setAttribute( 13483 "aria-atomic", 13484 "true", 13485 ); 13486 this.successMsg.setAttribute("tabindex", "-1"); 13487 } 13488 13489 this.fileList = this.form.querySelector( 13490 "[data-files-list]", 13491 ); 13492 13493 this.fileClearBtn = this.form.querySelector( 13494 "[data-files-clear]", 13495 ); 13496 }; 13497 13498 ContactFormController.prototype.bindEvents = function () { 13499 var self = this; 13500
13501 Object.keys(this.fields).forEach(function (fieldName) { 13502 var field = self.fields[fieldName]; 13503 13504 if ( 13505 !field || 13506 fieldName === "files" || 13507 fieldName === "recaptcha" 13508 ) { 13509 return; 13510 } 13511 13512 field.addEventListener("blur", function () { 13513 self.validateField(fieldName); 13514 }); 13515 13516 field.addEventListener("change", function () { 13517 self.validateField(fieldName); 13518 }); 13519 13520 field.addEventListener( 13521 "input", 13522 debounce(function () { 13523 if ( 13524 field.getAttribute("aria-invalid") === 13525 "true" 13526 ) { 13527 self.validateField(fieldName); 13528 } 13529 }, 120), 13530 ); 13531 }); 13532 13533 if (this.fields.files) { 13534 this.fields.files.addEventListener( 13535 "change", 13536 function () { 13537 self.validateField("files"); 13538 self.updateFileList(); 13539 }, 13540 ); 13541 13542 var fileWrapper = this.form.querySelector( 13543 "[data-files-wrapper]", 13544 ); 13545 13546 if (fileWrapper) { 13547 [ 13548 "dragenter", 13549 "dragover", 13550 "dragleave", 13551 "drop", 13552 ].forEach(function (eventName) { 13553 fileWrapper.addEventListener( 13554 eventName, 13555 function (event) { 13556 self.handleFileDragDrop( 13557 event, 13558 fileWrapper, 13559 ); 13560 }, 13561 ); 13562 }); 13563 } 13564 } 13565 13566 if ( 13567 this.fileClearBtn && 13568 this.fields.files 13569 ) { 13570 this.fileClearBtn.addEventListener( 13571 "click", 13572 function () { 13573 self.fields.files.value = ""; 13574 self.updateFileList(); 13575 self.clearFieldError("files"); 13576 }, 13577 ); 13578 } 13579 13580 if (this.fields.privacy) { 13581 this.fields.privacy.addEventListener( 13582 "keydown", 13583 function (event) { 13584 if (event.key !== "Enter") return; 13585 13586 event.preventDefault(); 13587 event.stopImmediatePropagation(); 13588 event.currentTarget.click(); 13589 }, 13590 true, 13591 ); 13592 } 13593 13594 this.form.addEventListener( 13595 "submit", 13596 function (event) { 13597 event.preventDefault(); 13598 self.handleSubmit(); 13599 }, 13600 ); 13601 }; 13602 13603 ContactFormController.prototype.initRecaptcha = 13604 function () { 13605 var recaptchaField = this.form.querySelector( 13606 "[data-recaptcha-field]", 13607 ); 13608 13609 var widget = this.form.querySelector( 13610 "[data-recaptcha-widget]", 13611 ); 13612 13613 var siteKey = this.form.getAttribute( 13614 "data-recaptcha-site-key", 13615 ); 13616 13617 if ( 13618 !recaptchaField || 13619 !widget || 13620 !siteKey 13621 ) { 13622 return; 13623 } 13624 13625 var self = this; 13626 13627 function render() { 13628 if ( 13629 !window.grecaptcha || 13630 typeof window.grecaptcha.render !== "function" 13631 ) { 13632 return false; 13633 } 13634 13635 if ( 13636 widget.getAttribute( 13637 "data-recaptcha-rendered", 13638 ) === "true" 13639 ) { 13640 return true; 13641 } 13642 13643 self.recaptchaWidgetId = 13644 window.grecaptcha.render(widget, { 13645 sitekey: siteKey, 13646 callback: function (token) { 13647 if (self.fields.recaptcha) { 13648 self.fields.recaptcha.value = 13649 token || ""; 13650 } 13651 13652 self.clearFieldError("recaptcha"); 13653 }, 13654 "expired-callback": function () { 13655 if (self.fields.recaptcha) { 13656 self.fields.recaptcha.value = ""; 13657 } 13658 }, 13659 }); 13660 13661 widget.setAttribute( 13662 "data-recaptcha-rendered", 13663 "true", 13664 ); 13665 13666 return true; 13667 } 13668 13669 if (!render()) { 13670 document.addEventListener( 13671 RECAPTCHA_READY_EVENT, 13672 render, 13673 { once: true }, 13674 ); 13675 13676 loadRecaptchaScript(); 13677 13678 var tries = 0; 13679 13680 var timer = window.setInterval(function () { 13681 tries += 1; 13682 13683 if (render() || tries >= 20) { 13684 window.clearInterval(timer); 13685 } 13686 }, 250); 13687 } 13688 }; 13689 13690 ContactFormController.prototype.resetRecaptcha = 13691 function () { 13692 if ( 13693 !window.grecaptcha || 13694 this.recaptchaWidgetId === null 13695 ) { 13696 return; 13697 } 13698
13699 try { 13700 window.grecaptcha.reset( 13701 this.recaptchaWidgetId, 13702 ); 13703 } catch (error) {} 13704 13705 if (this.fields.recaptcha) { 13706 this.fields.recaptcha.value = ""; 13707 } 13708 }; 13709 13710 ContactFormController.prototype.initCustomSelects = 13711 function () { 13712 var self = this; 13713 13714 var wrappers = this.form.querySelectorAll( 13715 "[data-enisa-select]", 13716 ); 13717 13718 wrappers.forEach(function (wrapper) { 13719 if ( 13720 wrapper.getAttribute( 13721 "data-enisa-select-init", 13722 ) === "true" 13723 ) { 13724 return; 13725 } 13726 13727 var select = wrapper.querySelector("select"); 13728 13729 var trigger = wrapper.querySelector( 13730 ".enisa-select-control__trigger", 13731 ); 13732 13733 var label = wrapper.querySelector( 13734 ".enisa-select-control__label", 13735 ); 13736 13737 var menu = wrapper.querySelector( 13738 ".enisa-select-control__menu", 13739 ); 13740 13741 if ( 13742 !select || 13743 !trigger || 13744 !label || 13745 !menu 13746 ) { 13747 return; 13748 } 13749 13750 wrapper.setAttribute( 13751 "data-enisa-select-init", 13752 "true", 13753 ); 13754 13755 select.setAttribute("tabindex", "-1"); 13756 select.setAttribute("aria-hidden", "true"); 13757 13758 if (!label.id) { 13759 label.id = 13760 (select.id || 13761 "enisa-contact-select") + 13762 "-value"; 13763 } 13764 13765 var fieldLabel = select.id 13766 ? self.form.querySelector( 13767 "label[for='" + 13768 select.id.replace(/'/g, "\\'") + 13769 "']", 13770 ) 13771 : null; 13772 13773 if (fieldLabel && !fieldLabel.id) { 13774 fieldLabel.id = 13775 select.id + "-label"; 13776 } 13777 13778 var accessibleLabelId = fieldLabel 13779 ? fieldLabel.id 13780 : label.id; 13781 13782 trigger.setAttribute( 13783 "aria-labelledby", 13784 accessibleLabelId, 13785 ); 13786 13787 trigger.setAttribute( 13788 "aria-haspopup", 13789 "listbox", 13790 ); 13791 13792 trigger.setAttribute( 13793 "aria-expanded", 13794 "false", 13795 ); 13796 13797 trigger.setAttribute( 13798 "aria-controls", 13799 menu.id || 13800 select.id + "-listbox", 13801 ); 13802 13803 if ( 13804 select.getAttribute( 13805 "aria-describedby", 13806 ) 13807 ) { 13808 trigger.setAttribute( 13809 "aria-describedby", 13810 select.getAttribute( 13811 "aria-describedby", 13812 ), 13813 ); 13814 } 13815 13816 if (select.required) { 13817 trigger.setAttribute( 13818 "aria-required", 13819 "true", 13820 ); 13821 } else { 13822 trigger.removeAttribute( 13823 "aria-required", 13824 ); 13825 } 13826 13827 if (!menu.id) { 13828 menu.id = 13829 select.id + "-listbox"; 13830 } 13831 13832 menu.setAttribute("role", "listbox"); 13833 13834 menu.setAttribute( 13835 "aria-labelledby", 13836 accessibleLabelId, 13837 ); 13838 13839 menu.hidden = true; 13840 13841 function syncLabel() { 13842 var selected = 13843 getSelectedOption(select); 13844 13845 var selectedText = 13846 selected && selected.value 13847 ? trimValue( 13848 selected.textContent, 13849 ) 13850 : "Selecciona una opción"; 13851 13852 label.textContent = selectedText; 13853 } 13854 13855 function closeMenu() { 13856 wrapper.classList.remove("is-open"); 13857 13858 trigger.setAttribute( 13859 "aria-expanded", 13860 "false", 13861 ); 13862 13863 menu.hidden = true; 13864 } 13865 13866 function openMenu() { 13867 wrapper.classList.add("is-open"); 13868 13869 trigger.setAttribute( 13870 "aria-expanded", 13871 "true", 13872 ); 13873 13874 menu.hidden = false; 13875 } 13876 13877 function getOptions() { 13878 return Array.prototype.slice.call( 13879 menu.querySelectorAll( 13880 ".enisa-select-control__option", 13881 ), 13882 ); 13883 } 13884 13885 function focusOption(index) { 13886 var options = getOptions(); 13887 13888 if (!options.length) return; 13889 13890 var safeIndex = Math.max( 13891 0, 13892 Math.min( 13893 index, 13894 options.length - 1, 13895 ), 13896 ); 13897 13898 safeFocus(options[safeIndex]); 13899 } 13900 13901 function getSelectedIndex() { 13902 var options = getOptions(); 13903 13904 var selectedValue = 13905 getSelectedOptionValue(select); 13906 13907 for ( 13908 var i = 0; 13909 i < options.length; 13910 i += 1 13911 ) { 13912 if ( 13913 options[i].getAttribute( 13914 "data-value", 13915 ) === selectedValue 13916 ) { 13917 return i; 13918 } 13919 } 13920 13921 return 0; 13922 } 13923 13924 function buildMenu() { 13925 menu.innerHTML = ""; 13926 13927 Array.prototype.forEach.call( 13928 select.options, 13929 function (option, index) { 13930 if ( 13931 option.disabled && 13932 !option.value 13933 ) { 13934 return; 13935 } 13936 13937 var item = 13938 document.createElement("div"); 13939 13940 item.className = 13941 "enisa-select-control__option"; 13942 13943 item.setAttribute( 13944 "role", 13945 "option", 13946 ); 13947 13948 item.setAttribute( 13949 "data-value", 13950 option.value, 13951 ); 13952 13953 item.setAttribute( 13954 "tabindex", 13955 "-1", 13956 ); 13957 13958 item.setAttribute( 13959 "aria-selected", 13960 option.selected 13961 ? "true" 13962 : "false", 13963 ); 13964 13965 item.textContent = trimValue( 13966 option.textContent, 13967 ); 13968 13969 if (option.disabled) { 13970 item.setAttribute( 13971 "aria-disabled", 13972 "true", 13973 ); 13974 } 13975 13976 item.addEventListener( 13977 "click", 13978 function () { 13979 if (option.disabled) return; 13980 13981 select.selectedIndex = index; 13982 13983 select.dispatchEvent( 13984 new Event("change", { 13985 bubbles: true, 13986 }), 13987 ); 13988 13989 syncLabel(); 13990 buildMenu(); 13991 closeMenu(); 13992 safeFocus(trigger); 13993 13994 self.validateField( 13995 "department", 13996 ); 13997 }, 13998 ); 13999 14000 item.addEventListener( 14001 "keydown", 14002 function (event) { 14003 var options = getOptions(); 14004 14005 var currentIndex = 14006 options.indexOf(item); 14007 14008 if ( 14009 event.key === 14010 "ArrowDown" 14011 ) { 14012 event.preventDefault(); 14013 14014 focusOption( 14015 currentIndex + 1, 14016 ); 14017 14018 return; 14019 } 14020 14021 if ( 14022 event.key === "ArrowUp" 14023 ) { 14024 event.preventDefault(); 14025 14026 focusOption( 14027 currentIndex - 1, 14028 ); 14029 14030 return; 14031 } 14032 14033 if ( 14034 event.key === "Home" 14035 ) { 14036 event.preventDefault(); 14037 focusOption(0); 14038 return; 14039 } 14040 14041 if ( 14042 event.key === "End" 14043 ) { 14044 event.preventDefault(); 14045 14046 focusOption( 14047 options.length - 1, 14048 ); 14049 14050 return; 14051 } 14052 14053 if ( 14054 event.key === "Escape" 14055 ) { 14056 event.preventDefault(); 14057 closeMenu(); 14058 safeFocus(trigger); 14059 return; 14060 } 14061 14062 if ( 14063 event.key === "Enter" || 14064 event.key === " " 14065 ) { 14066 event.preventDefault(); 14067 item.click(); 14068 } 14069 }, 14070 ); 14071 14072 menu.appendChild(item); 14073 }, 14074 ); 14075 } 14076 14077 trigger.addEventListener( 14078 "click", 14079 function () { 14080 if ( 14081 wrapper.classList.contains( 14082 "is-open", 14083 ) 14084 ) { 14085 closeMenu(); 14086 } else { 14087 openMenu(); 14088 } 14089 }, 14090 ); 14091 14092 trigger.addEventListener( 14093 "keydown", 14094 function (event) { 14095 if ( 14096 event.key === "ArrowDown" || 14097 event.key === "ArrowUp" 14098 ) { 14099 event.preventDefault(); 14100 openMenu(); 14101 14102 focusOption( 14103 getSelectedIndex(), 14104 ); 14105 14106 return; 14107 } 14108 14109 if ( 14110 event.key === "Enter" || 14111 event.key === " " 14112 ) { 14113 event.preventDefault(); 14114 14115 if ( 14116 wrapper.classList.contains( 14117 "is-open", 14118 ) 14119 ) { 14120 closeMenu(); 14121 } else { 14122 openMenu(); 14123 14124 focusOption( 14125 getSelectedIndex(), 14126 ); 14127 } 14128 14129 return; 14130 } 14131 14132 if (event.key === "Escape") { 14133 closeMenu(); 14134 } 14135 }, 14136 ); 14137 14138 document.addEventListener( 14139 "click", 14140 function (event) { 14141 if ( 14142 wrapper.contains(event.target) 14143 ) { 14144 return; 14145 } 14146 14147 closeMenu(); 14148 }, 14149 ); 14150 14151 select.addEventListener( 14152 "change", 14153 function () { 14154 syncLabel(); 14155 buildMenu(); 14156 }, 14157 ); 14158 14159 syncLabel(); 14160 buildMenu(); 14161 }); 14162 }; 14163 14164 ContactFormController.prototype.handleFileDragDrop = 14165 function (event, wrapper) { 14166 event.preventDefault(); 14167 event.stopPropagation(); 14168 14169 if (!this.fields.files) return; 14170 14171 if ( 14172 event.type === "dragenter" || 14173 event.type === "dragover" 14174 ) { 14175 wrapper.classList.add( 14176 "is-dragging", 14177 ); 14178 14179 return; 14180 } 14181 14182 wrapper.classList.remove( 14183 "is-dragging", 14184 ); 14185 14186 if ( 14187 event.type !== "drop" || 14188 !event.dataTransfer || 14189 !event.dataTransfer.files 14190 ) { 14191 return; 14192 } 14193 14194 this.fields.files.files = 14195 event.dataTransfer.files; 14196 14197 this.validateField("files"); 14198 this.updateFileList(); 14199 }; 14200 14201 ContactFormController.prototype.setFieldError = 14202 function (fieldName, message) { 14203 var field = this.fields[fieldName]; 14204 14205 var errorNode = this.form.querySelector( 14206 "[data-error-" + 14207 fieldName + 14208 "]", 14209 ); 14210 14211 if (field) { 14212 field.setAttribute( 14213 "aria-invalid", 14214 "true", 14215 ); 14216 } 14217 14218 var selectRoot = field 14219 ? field.closest( 14220 ".enisa-select-control", 14221 ) 14222 : null; 14223 14224 var selectTrigger = selectRoot 14225 ? selectRoot.querySelector( 14226 ".enisa-select-control__trigger", 14227 ) 14228 : null; 14229 14230 if (selectTrigger) { 14231 selectTrigger.setAttribute( 14232 "aria-invalid", 14233 "true", 14234 ); 14235 } 14236 14237 var fieldContainer = field 14238 ? field.closest( 14239 ".enisa-ayuda-contacto__field", 14240 ) || 14241 field.closest("[data-field]") 14242 : null; 14243 14244 if (fieldContainer) { 14245 fieldContainer.classList.add( 14246 "has-error", 14247 ); 14248 } 14249 14250 if (selectRoot) { 14251 selectRoot.classList.add( 14252 "is-error", 14253 ); 14254 } 14255 14256 if (errorNode) { 14257 errorNode.textContent = 14258 message || ""; 14259 14260 errorNode.hidden = !message; 14261 } 14262 }; 14263 14264 ContactFormController.prototype.clearFieldError = 14265 function (fieldName) { 14266 var field = this.fields[fieldName]; 14267 14268 var errorNode = this.form.querySelector( 14269 "[data-error-" + 14270 fieldName + 14271 "]", 14272 ); 14273 14274 if (field) { 14275 field.setAttribute( 14276 "aria-invalid", 14277 "false", 14278 ); 14279 } 14280 14281 var selectRoot = field 14282 ? field.closest( 14283 ".enisa-select-control", 14284 ) 14285 : null; 14286 14287 var selectTrigger = selectRoot 14288 ? selectRoot.querySelector( 14289 ".enisa-select-control__trigger", 14290 ) 14291 : null; 14292 14293 if (selectTrigger) { 14294 selectTrigger.setAttribute( 14295 "aria-invalid", 14296 "false", 14297 ); 14298 } 14299 14300 var fieldContainer = field 14301 ? field.closest( 14302 ".enisa-ayuda-contacto__field", 14303 ) || 14304 field.closest("[data-field]") 14305 : null; 14306 14307 if (fieldContainer) { 14308 fieldContainer.classList.remove( 14309 "has-error", 14310 ); 14311 } 14312 14313 if (selectRoot) { 14314 selectRoot.classList.remove( 14315 "is-error", 14316 ); 14317 } 14318 14319 if (errorNode) { 14320 errorNode.textContent = ""; 14321 errorNode.hidden = true; 14322 } 14323 }; 14324 14325 ContactFormController.prototype.validateField = 14326 function (fieldName) { 14327 var field = this.fields[fieldName]; 14328 14329 if (!field) return true; 14330 14331 var value = 14332 field.type === "checkbox" 14333 ? field.checked 14334 : trimValue(field.value); 14335 14336 var message = ""; 14337 14338 switch (fieldName) { 14339 case "name": 14340 if (!value) { 14341 message = 14342 "Introduce tu nombre."; 14343 } 14344 break; 14345 14346 case "lastname": 14347 if (!value) { 14348 message = 14349 "Introduce tus apellidos."; 14350 } 14351 break; 14352 14353 case "email": 14354 if (!value) { 14355 message = 14356 "Introduce tu correo electrónico."; 14357 } else if (!isValidEmail(value)) { 14358 message = 14359 "Introduce un correo electrónico válido."; 14360 } 14361 break; 14362 14363 case "department": 14364 if (!value) { 14365 message = 14366 "Selecciona un motivo."; 14367 } 14368 break; 14369 14370 case "message": 14371 if (!value) { 14372 message = 14373 "Escribe tu consulta."; 14374 } 14375 break; 14376 14377 case "privacy": 14378 if (!field.checked) { 14379 message = 14380 "Debes aceptar la polÃtica de privacidad."; 14381 } 14382 break; 14383 14384 case "files": 14385 message = this.validateFiles(); 14386 break; 14387 14388 case "recaptcha": 14389 if ( 14390 this.form.hasAttribute( 14391 "data-recaptcha-site-key", 14392 ) && 14393 !trimValue(field.value) 14394 ) { 14395 message = 14396 "Completa la verificación anti-spam."; 14397 } 14398 break; 14399 } 14400 14401 if (message) { 14402 this.setFieldError( 14403 fieldName, 14404 message, 14405 ); 14406 14407 return false;
14408 } 14409 14410 this.clearFieldError(fieldName); 14411 14412 return true; 14413 }; 14414 14415 ContactFormController.prototype.validateFiles = 14416 function () { 14417 var fileInput = this.fields.files; 14418 14419 if ( 14420 !fileInput || 14421 !fileInput.files || 14422 !fileInput.files.length 14423 ) { 14424 return ""; 14425 } 14426 14427 var allowedExtensions = 14428 getAllowedExtensions(fileInput); 14429 14430 var maxSize = 14431 getMaxFileSize(fileInput); 14432 14433 for ( 14434 var i = 0; 14435 i < fileInput.files.length; 14436 i += 1 14437 ) { 14438 var file = fileInput.files[i]; 14439 14440 var extension = 14441 getFileExtension(file.name); 14442 14443 if ( 14444 allowedExtensions.length && 14445 allowedExtensions.indexOf( 14446 extension, 14447 ) === -1 14448 ) { 14449 return ( 14450 "El archivo " + 14451 file.name + 14452 " no tiene un formato permitido." 14453 ); 14454 } 14455 14456 if (file.size > maxSize) { 14457 return ( 14458 "El archivo " + 14459 file.name + 14460 " supera el tamaño máximo de " + 14461 bytesToMb(maxSize) + 14462 " MB." 14463 ); 14464 } 14465 } 14466 14467 return ""; 14468 }; 14469 14470 ContactFormController.prototype.validateForm = 14471 function () { 14472 var requiredFields = [ 14473 "name", 14474 "lastname", 14475 "email", 14476 "department", 14477 "message", 14478 "privacy", 14479 ]; 14480 14481 var isValid = true; 14482 var firstInvalid = null; 14483 14484 if (this.fields.files) { 14485 requiredFields.push("files"); 14486 } 14487 14488 if ( 14489 this.fields.recaptcha && 14490 this.form.hasAttribute( 14491 "data-recaptcha-site-key", 14492 ) 14493 ) { 14494 requiredFields.push("recaptcha"); 14495 } 14496 14497 for ( 14498 var i = 0; 14499 i < requiredFields.length; 14500 i += 1 14501 ) { 14502 var fieldName = 14503 requiredFields[i]; 14504 14505 var fieldValid = 14506 this.validateField(fieldName); 14507 14508 if ( 14509 !fieldValid && 14510 !firstInvalid 14511 ) { 14512 firstInvalid = 14513 this.fields[fieldName]; 14514 } 14515 14516 isValid = 14517 fieldValid && isValid; 14518 } 14519 14520 if ( 14521 !isValid && 14522 firstInvalid && 14523 typeof firstInvalid.focus === 14524 "function" 14525 ) { 14526 var selectRoot = 14527 firstInvalid.closest( 14528 ".enisa-select-control", 14529 ); 14530 14531 var selectTrigger = selectRoot 14532 ? selectRoot.querySelector( 14533 ".enisa-select-control__trigger", 14534 ) 14535 : null; 14536 14537 var focusTarget = 14538 selectTrigger || firstInvalid; 14539 14540 safeFocus(focusTarget); 14541 } 14542 14543 return isValid; 14544 }; 14545 14546 ContactFormController.prototype.updateFileList = 14547 function () { 14548 var fileInput = 14549 this.fields.files; 14550 14551 var files = 14552 fileInput && fileInput.files 14553 ? Array.prototype.slice.call( 14554 fileInput.files, 14555 ) 14556 : []; 14557 14558 var wrapper = this.form.querySelector( 14559 "[data-files-wrapper]", 14560 ); 14561 14562 var fileText = wrapper 14563 ? wrapper.querySelector( 14564 ".enisa-ayuda-contacto__file-text", 14565 ) 14566 : null; 14567 14568 var formats = wrapper 14569 ? wrapper.querySelector( 14570 ".enisa-ayuda-contacto__file-formats", 14571 ) 14572 : null; 14573 14574 if (fileText) { 14575 if (files.length) { 14576 fileText.textContent = 14577 files.length + 14578 " archivo" + 14579 (files.length === 1 14580 ? "" 14581 : "s") + 14582 " seleccionado" + 14583 (files.length === 1 14584 ? "" 14585 : "s"); 14586 } else { 14587 fileText.innerHTML = 14588 '<span class="enisa-ayuda-contacto__file-link">Subir archivos</span>'; 14589 } 14590 } 14591 14592 if (formats) { 14593 formats.hidden = 14594 files.length > 0; 14595 } 14596 14597 if (this.fileClearBtn) { 14598 this.fileClearBtn.hidden = 14599 files.length === 0; 14600 } 14601 14602 if (!this.fileList) return; 14603 14604 this.fileList.innerHTML = ""; 14605 14606 this.fileList.hidden = 14607 files.length === 0; 14608
14609 files.forEach( 14610 function (file) { 14611 var item = 14612 document.createElement("li"); 14613 14614 item.className = 14615 "enisa-ayuda-contacto__file-item"; 14616 14617 item.textContent = 14618 file.name + 14619 " (" + 14620 bytesToMb(file.size) + 14621 " MB)"; 14622 14623 this.fileList.appendChild( 14624 item, 14625 ); 14626 }, 14627 this, 14628 ); 14629 }; 14630 14631 ContactFormController.prototype.getTargetEmail = 14632 function (departmentKey) { 14633 if (!departmentKey) return ""; 14634 14635 var mapped = 14636 this.departmentEmailMap[ 14637 departmentKey 14638 ]; 14639 14640 if (mapped) { 14641 return trimValue(mapped); 14642 } 14643 14644 return ""; 14645 }; 14646 14647 ContactFormController.prototype.buildContactPayload = 14648 function () { 14649 var name = trimValue( 14650 this.fields.name && 14651 this.fields.name.value, 14652 ); 14653 14654 var lastname = trimValue( 14655 this.fields.lastname && 14656 this.fields.lastname.value, 14657 ); 14658 14659 var email = normalizeEmail( 14660 this.fields.email && 14661 this.fields.email.value, 14662 ); 14663 14664 var departmentKey = 14665 getSelectedOptionValue( 14666 this.fields.department, 14667 ); 14668 14669 var departmentLabel = 14670 getSelectedOptionLabel( 14671 this.fields.department, 14672 ); 14673 14674 var message = trimValue( 14675 this.fields.message && 14676 this.fields.message.value, 14677 ); 14678 14679 var targetEmail = 14680 this.getTargetEmail( 14681 departmentKey, 14682 ); 14683 14684 var payload = {}; 14685 14686 payload[CONTACT_FIELDS.name] = 14687 trimValue( 14688 name + " " + lastname, 14689 ); 14690 14691 payload[CONTACT_FIELDS.lastname] = 14692 lastname; 14693 14694 payload[CONTACT_FIELDS.email] = 14695 email; 14696 14697 payload[ 14698 CONTACT_FIELDS.departmentKey 14699 ] = departmentKey; 14700 14701 payload[ 14702 CONTACT_FIELDS.departmentLabel 14703 ] = departmentLabel; 14704 14705 payload[CONTACT_FIELDS.message] = 14706 message; 14707 14708 payload[ 14709 CONTACT_FIELDS.privacyAccepted 14710 ] = !!( 14711 this.fields.privacy && 14712 this.fields.privacy.checked 14713 ); 14714 14715 payload[ 14716 CONTACT_FIELDS.targetEmail 14717 ] = targetEmail; 14718 14719 return payload; 14720 }; 14721 14722 ContactFormController.prototype.createContact = 14723 function () { 14724 var entitySet = trimValue( 14725 this.form.getAttribute( 14726 "data-contact-entity-set", 14727 ), 14728 ); 14729 14730 var payload = 14731 this.buildContactPayload(); 14732 14733 if (!entitySet) { 14734 return Promise.reject( 14735 new Error( 14736 "No se ha configurado el entity set del formulario.", 14737 ), 14738 ); 14739 } 14740 14741 return webApiRequest({ 14742 method: "POST", 14743 url: 14744 "/_api/" + 14745 encodeURIComponent(entitySet), 14746 body: JSON.stringify(payload), 14747 headers: { 14748 Prefer: "return=representation", 14749 }, 14750 }).then(function (result) { 14751 var entityIdHeader = 14752 result.getResponseHeader( 14753 "OData-EntityId", 14754 ) || 14755 result.getResponseHeader( 14756 "odata-entityid", 14757 ) || 14758 result.getResponseHeader( 14759 "Location", 14760 ) || 14761 ""; 14762 14763 var id = 14764 extractGuidFromEntityHeader( 14765 entityIdHeader, 14766 ); 14767 14768 if (!id && result.data) { 14769 id = 14770 result.data 14771 .enisa_contactoid || 14772 result.data 14773 .enisa_contactoId || 14774 result.data.id || 14775 ""; 14776 } 14777 14778 return { 14779 id: id, 14780 payload: payload, 14781 }; 14782 }); 14783 }; 14784 14785 ContactFormController.prototype.createAnnotation = 14786 function (contactId, file) { 14787 var annotationEntitySet = 14788 trimValue( 14789 this.form.getAttribute( 14790 "data-annotation-entity-set", 14791 ), 14792 ) || "annotations"; 14793 14794 var contactEntitySet = 14795 trimValue( 14796 this.form.getAttribute( 14797 "data-contact-entity-set", 14798 ), 14799 ); 14800 14801 var bindField = 14802 trimValue( 14803 this.form.getAttribute( 14804 "data-annotation-bind-field", 14805 ), 14806 ) || 14807 "[email protected]"; 14808 14809 if ( 14810 !contactId || 14811 !file || 14812 !contactEntitySet 14813 ) { 14814 return Promise.resolve(); 14815 } 14816 14817 return readFileAsBase64( 14818 file, 14819 ).then(function (base64) { 14820 var payload = { 14821 subject: file.name, 14822 filename: file.name, 14823 mimetype: 14824 file.type || 14825 "application/octet-stream", 14826 documentbody: base64, 14827 }; 14828 14829 payload[bindField] = 14830 "/" + 14831 contactEntitySet + 14832 "(" + 14833 contactId + 14834 ")"; 14835 14836 return webApiRequest({ 14837 method: "POST", 14838 url: 14839 "/_api/" + 14840 encodeURIComponent( 14841 annotationEntitySet, 14842 ), 14843 body: JSON.stringify(payload), 14844 }); 14845 }); 14846 }; 14847 14848 ContactFormController.prototype.createAnnotations = 14849 function (contactId) { 14850 var fileInput = 14851 this.fields.files; 14852 14853 var files = 14854 fileInput && fileInput.files 14855 ? Array.prototype.slice.call( 14856 fileInput.files, 14857 ) 14858 : []; 14859 14860 if (!files.length) { 14861 return Promise.resolve(); 14862 } 14863 14864 var self = this; 14865 14866 return files.reduce( 14867 function (promise, file) { 14868 return promise.then( 14869 function () { 14870 return self.createAnnotation( 14871 contactId, 14872 file, 14873 ); 14874 }, 14875 ); 14876 }, 14877 Promise.resolve(), 14878 ); 14879 }; 14880 14881 ContactFormController.prototype.setSubmittingState = 14882 function (isSubmitting) { 14883 this.isSubmitting = 14884 isSubmitting; 14885 14886 if (this.submitBtn) { 14887 setSubmitButtonState( 14888 this.submitBtn, 14889 isSubmitting 14890 ? "submitting" 14891 : "idle", 14892 ); 14893 } 14894 14895 if (this.loadingMsg) { 14896 this.loadingMsg.hidden = 14897 !isSubmitting; 14898 } 14899 14900 if ( 14901 this.successMsg && 14902 isSubmitting 14903 ) { 14904 this.successMsg.hidden = true; 14905 } 14906 }; 14907 14908 ContactFormController.prototype.showError = 14909 function (message) { 14910 if (!this.errorSummary) return; 14911 14912 this.errorSummary.textContent = 14913 message || 14914 DEFAULT_ERROR_MESSAGE; 14915 14916 this.errorSummary.hidden = false;
14917 }; 14918 14919 ContactFormController.prototype.focusErrorSummary = 14920 function () { 14921 if ( 14922 !this.errorSummary || 14923 this.errorSummary.hidden 14924 ) { 14925 return; 14926 } 14927 14928 if ( 14929 window.ENISA && 14930 window.ENISA.utils && 14931 typeof window.ENISA.utils 14932 .focusWithoutScroll === 14933 "function" 14934 ) { 14935 window.ENISA.utils.focusWithoutScroll( 14936 this.errorSummary, 14937 ); 14938 14939 return; 14940 } 14941 14942 safeFocus(this.errorSummary); 14943 }; 14944 14945 ContactFormController.prototype.clearError = 14946 function () { 14947 if (!this.errorSummary) return; 14948 14949 this.errorSummary.textContent = ""; 14950 this.errorSummary.hidden = true; 14951 }; 14952 14953 ContactFormController.prototype.showSuccess = 14954 function () { 14955 if (!this.successMsg) return; 14956 14957 this.successMsg.textContent = 14958 DEFAULT_SUCCESS_MESSAGE; 14959 14960 this.successMsg.hidden = false; 14961 14962 setSubmitButtonState( 14963 this.submitBtn, 14964 "success", 14965 ); 14966 }; 14967 14968 ContactFormController.prototype.clearAllErrors = 14969 function () { 14970 var self = this; 14971 14972 Object.keys(this.fields).forEach( 14973 function (fieldName) { 14974 self.clearFieldError( 14975 fieldName, 14976 ); 14977 }, 14978 ); 14979 14980 this.clearError(); 14981 }; 14982 14983 ContactFormController.prototype.resetForm = 14984 function () { 14985 this.form.reset(); 14986 14987 this.form 14988 .querySelectorAll( 14989 "[data-enisa-select]", 14990 ) 14991 .forEach(function (wrapper) { 14992 var select = 14993 wrapper.querySelector( 14994 "select", 14995 ); 14996 14997 var label = 14998 wrapper.querySelector( 14999 ".enisa-select-control__label", 15000 ); 15001 15002 if (select) { 15003 select.selectedIndex = 0; 15004 } 15005 15006 if (label) { 15007 label.textContent = 15008 "Selecciona una opción"; 15009 } 15010 }); 15011 15012 this.updateFileList(); 15013 this.resetRecaptcha(); 15014 this.clearAllErrors(); 15015 15016 setSubmitButtonState( 15017 this.submitBtn, 15018 "idle", 15019 ); 15020 }; 15021 15022 ContactFormController.prototype.handleSubmit = 15023 function () { 15024 var self = this; 15025 15026 if (this.isSubmitting) return; 15027 15028 this.clearError(); 15029 15030 if (!this.validateForm()) { 15031 this.showError( 15032 "Revisa los campos marcados antes de enviar el formulario.", 15033 ); 15034 15035 return; 15036 } 15037 15038 this.setSubmittingState(true); 15039 15040 this.createContact() 15041 .then(function (contact) { 15042 return self.createAnnotations( 15043 contact.id, 15044 ); 15045 }) 15046 .then(function () { 15047 self.isSubmitting = false; 15048 15049 if (self.loadingMsg) { 15050 self.loadingMsg.hidden = true; 15051 } 15052 15053 self.showSuccess(); 15054 15055 window.setTimeout( 15056 function () { 15057 self.resetForm(); 15058 15059 if (self.successMsg) { 15060 self.successMsg.hidden = 15061 true; 15062 } 15063 15064 if (self.submitBtn) { 15065 self.submitBtn.hidden = 15066 false; 15067 15068 setSubmitButtonState( 15069 self.submitBtn, 15070 "idle", 15071 ); 15072 } 15073 }, 15074 3000, 15075 ); 15076 }) 15077 .catch(function (error) { 15078 self.setSubmittingState(false); 15079 15080 if (self.submitBtn) { 15081 self.submitBtn.hidden = false; 15082 15083 setSubmitButtonState( 15084 self.submitBtn, 15085 "idle", 15086 ); 15087 } 15088 15089 self.showError( 15090 error && error.message 15091 ? error.message 15092 : DEFAULT_ERROR_MESSAGE, 15093 ); 15094 }); 15095 }; 15096 15097 function initContactForms() { 15098 var roots = 15099 document.querySelectorAll( 15100 ROOT_SELECTOR, 15101 ); 15102 15103 Array.prototype.forEach.call( 15104 roots, 15105 function (root) { 15106 if ( 15107 root.getAttribute( 15108 INIT_ATTR, 15109 ) === "true" 15110 ) { 15111 return; 15112 } 15113 15114 root.setAttribute( 15115 INIT_ATTR, 15116 "true", 15117 ); 15118 15119 var controller = 15120 new ContactFormController(root); 15121 15122 controller.init(); 15123 }, 15124 ); 15125 } 15126 15127 ready(initContactForms); 15128 15129 if ("MutationObserver" in window) { 15130 var observerTimer = null; 15131 15132 var observer = 15133 new MutationObserver( 15134 function (mutations) { 15135 var hasContactForm = 15136 mutations.some( 15137 function (mutation) { 15138 return Array.prototype.some.call( 15139 mutation.addedNodes, 15140 function (node) { 15141 if ( 15142 !node || 15143 node.nodeType !== 1 15144 ) { 15145 return false;
15146 } 15147 15148 return ( 15149 (node.matches && 15150 node.matches( 15151 ROOT_SELECTOR, 15152 )) || 15153 (node.querySelector && 15154 node.querySelector( 15155 ROOT_SELECTOR, 15156 )) 15157 ); 15158 }, 15159 ); 15160 }, 15161 ); 15162 15163 if (!hasContactForm) return; 15164 15165 window.clearTimeout( 15166 observerTimer, 15167 ); 15168 15169 observerTimer = 15170 window.setTimeout( 15171 initContactForms, 15172 150, 15173 ); 15174 }, 15175 ); 15176 15177 observer.observe(document.body, { 15178 childList: true, 15179 subtree: true, 15180 }); 15181 } 15182})(); 15183 15184/* 15185 ENISA | JS Module 15186 Modulo: 30-eligibility-wizard 15187 Descripcion: Logica accesible del test/minitest de elegibilidad. No contiene logica del formulario de contacto. 15188*/ 15189(function () { 15190 "use strict"; 15191 15192 var ROOT_SELECTOR = ".enisa-wizard.enisa-wizard--minitest"; 15193 var instanceCount = 0; 15194 15195 var DEFAULT_RESULT_COPY = { 15196 both: { 15197 title: "Tu empresa puede optar a financiación y certificación", 15198 text: "Según tus respuestas, tu pyme podrÃa cumplir los requisitos para acceder a financiación y a la certificación como empresa emergente. Revisa los detalles y si quieres comenzar el proceso, regÃstrate.", 15199 type: "ok" 15200 }, 15201 financingOnly: { 15202 title: "Tu empresa puede optar a financiación", 15203 text: "Según tus respuestas, tu pyme podrÃa cumplir los requisitos para acceder a nuestros préstamos. Revisa los detalles y si quieres comenzar el proceso, regÃstrate.", 15204 type: "ok" 15205 }, 15206 certificationOnly: { 15207 title: "Tu empresa puede certificarse como startup", 15208 text: "Según tus respuestas, tu pyme podrÃa cumplir los requisitos para acceder a la certificación de empresa emergente. Revisa los detalles y si quieres comenzar el proceso, regÃstrate.", 15209 type: "ok" 15210 }, 15211 none: { 15212 title: "Tu empresa no cumple con ningún requisito por ahora", 15213 text: "Según tus respuestas, tu empresa no encaja actualmente en nuestros criterios de financiación y certificación. Puedes revisar los requisitos por si quieres intentarlo de nuevo en otro momento.", 15214 type: "ko" 15215 } 15216 }; 15217 15218 function ready(callback) { 15219 if (document.readyState === "loading") { 15220 document.addEventListener("DOMContentLoaded", callback); 15221 return; 15222 } 15223 15224 callback(); 15225 } 15226 15227 function normalizeAnswerValue(value) { 15228 return String(value || "") 15229 .trim() 15230 .toLowerCase() 15231 .normalize("NFD") 15232 .replace(/[\u0300-\u036f]/g, ""); 15233 } 15234 15235 function isYesValue(value) { 15236 var normalized = normalizeAnswerValue(value); 15237 15238 return ( 15239 normalized === "si" || 15240 normalized === "yes" || 15241 normalized === "true" || 15242 normalized === "1" 15243 ); 15244 } 15245 15246 function decodeHtmlEntities(value) { 15247 if (!value || value.indexOf("&") === -1) return value || ""; 15248 15249 var textarea = document.createElement("textarea"); 15250 textarea.innerHTML = value; 15251 return textarea.value; 15252 } 15253 15254 function makeSafeId(value) { 15255 return String(value || "wizard") 15256 .toLowerCase() 15257 .replace(/[^a-z0-9\-_:.]+/g, "-") 15258 .replace(/^-+|-+$/g, "") || "wizard"; 15259 } 15260 15261 function setId(element, id) { 15262 if (!element || !id) return ""; 15263 15264 element.id = id; 15265 15266 return id; 15267 } 15268 15269 function focusElement(element) { 15270 if (!element || typeof element.focus !== "function") return; 15271 15272 window.requestAnimationFrame(function () { 15273 if ( 15274 window.ENISA && 15275 window.ENISA.utils && 15276 typeof window.ENISA.utils.safeFocus === "function" 15277 ) { 15278 window.ENISA.utils.safeFocus(element); 15279 return; 15280 } 15281 15282 try { 15283 element.focus({ preventScroll: true }); 15284 } catch (error) { 15285 element.focus(); 15286 } 15287 }); 15288 } 15289 15290 function parseJsonConfig(root) { 15291 var script = 15292 root.querySelector("#enisa-wizard-config") || 15293 document.getElementById("enisa-wizard-config"); 15294 15295 if (!script) return null; 15296 15297 var raw = script.textContent || ""; 15298 15299 if (!raw.trim()) return null; 15300 15301 var decoded = decodeHtmlEntities(raw); 15302 15303 function tryParse(value) {
15304 try { 15305 return JSON.parse(value); 15306 } catch (error) { 15307 return null; 15308 } 15309 } 15310 15311 var parsed = tryParse(decoded); 15312 15313 if (parsed) return parsed; 15314 15315 var cleaned = decoded 15316 .replace(/^\uFEFF/, "") 15317 .replace(/[ââ]/g, "\"") 15318 .replace(/[ââ]/g, "'") 15319 .trim(); 15320 15321 if (cleaned.indexOf("<") !== -1 && cleaned.indexOf(">") !== -1) { 15322 var div = document.createElement("div"); 15323 15324 div.innerHTML = cleaned; 15325 cleaned = (div.textContent || div.innerText || "").trim(); 15326 } 15327 15328 return tryParse(cleaned); 15329 } 15330 15331 function looksLikeQuestion(item) { 15332 if (!item || typeof item !== "object") return false; 15333 15334 return !!( 15335 item.question || 15336 item.title || 15337 item.pregunta || 15338 item.texto || 15339 item.text 15340 ); 15341 } 15342 15343 function findQuestionArray(config) { 15344 if (!config) return []; 15345 15346 if (Array.isArray(config)) return config; 15347 15348 var direct = 15349 config.questions || 15350 config.items || 15351 config.preguntas || 15352 config.steps || 15353 config.pasos; 15354 15355 if (Array.isArray(direct)) return direct; 15356 15357 if (direct && typeof direct === "object") { 15358 return Object.keys(direct) 15359 .sort(function (a, b) { 15360 var numA = parseInt(String(a).replace(/\D/g, ""), 10); 15361 var numB = parseInt(String(b).replace(/\D/g, ""), 10); 15362 15363 if (!isNaN(numA) && !isNaN(numB)) { 15364 return numA - numB; 15365 } 15366 15367 return String(a).localeCompare(String(b)); 15368 }) 15369 .map(function (key) { 15370 var item = direct[key] || {}; 15371 15372 if (!item.id) { 15373 item.id = key; 15374 } 15375 15376 return item; 15377 }); 15378 } 15379 15380 var keys = Object.keys(config); 15381 15382 for (var i = 0; i < keys.length; i += 1) { 15383 var value = config[keys[i]]; 15384 15385 if ( 15386 Array.isArray(value) && 15387 value.length && 15388 looksLikeQuestion(value[0]) 15389 ) { 15390 return value; 15391 } 15392 } 15393 15394 for (var j = 0; j < keys.length; j += 1) { 15395 var nested = config[keys[j]]; 15396 15397 if (!nested || typeof nested !== "object") continue; 15398 15399 var nestedFound = findQuestionArray(nested); 15400 15401 if (nestedFound.length) return nestedFound; 15402 } 15403 15404 return []; 15405 } 15406 15407 function normalizeOption(option, index) { 15408 if (typeof option === "string") { 15409 return { 15410 id: "opt-" + index, 15411 text: option, 15412 value: option 15413 }; 15414 } 15415 15416 option = option || {}; 15417 15418 return { 15419 id: 15420 option.id || 15421 option.key || 15422 option.value || 15423 "opt-" + index, 15424 text: 15425 option.text || 15426 option.label || 15427 option.title || 15428 option.texto || 15429 option.respuesta || 15430 "", 15431 value: 15432 option.value !== undefined 15433 ? option.value 15434 : option.id || 15435 option.key || 15436 option.text || 15437 option.label || 15438 option.texto || 15439 option.respuesta || 15440 "" 15441 }; 15442 } 15443 15444 function normalizeQuestion(question, index) { 15445 question = question || {}; 15446 15447 var rawOptions = 15448 question.options || 15449 question.answers || 15450 question.opciones || 15451 question.respuestas || 15452 []; 15453 15454 return { 15455 id: question.id || question.key || String(index + 1), 15456 kicker: 15457 question.kicker || 15458 "Pregunta " + (index + 1) + " de {total}", 15459 title: 15460 question.question || 15461 question.title || 15462 question.pregunta || 15463 question.text || 15464 question.texto || 15465 "", 15466 hint: question.hint || question.description || "", 15467 options: rawOptions.map(normalizeOption) 15468 }; 15469 } 15470 15471 function setHintContent(target, hint) { 15472 if (!target) return; 15473 15474 var raw = typeof hint === "string" ? hint.trim() : ""; 15475 15476 target.innerHTML = ""; 15477 15478 if (!raw) { 15479 target.textContent = ""; 15480 return; 15481 } 15482 15483 if (raw.indexOf("<") !== -1 && raw.indexOf(">") !== -1) { 15484 target.innerHTML = raw; 15485 return; 15486 } 15487 15488 var lines = raw 15489 .split(/\r?\n/) 15490 .map(function (line) { 15491 return line.trim(); 15492 }) 15493 .filter(Boolean); 15494 15495 if (!lines.length) { 15496 target.textContent = ""; 15497 return; 15498 } 15499 15500 if (lines.length === 1) { 15501 target.textContent = lines[0]; 15502 return; 15503 } 15504 15505 var intro = document.createElement("p"); 15506 15507 intro.textContent = lines[0]; 15508 target.appendChild(intro); 15509 15510 var list = document.createElement("ul"); 15511
15512 lines.slice(1).forEach(function (line) { 15513 var item = document.createElement("li"); 15514 15515 item.textContent = line.replace(/^[-*]\s*/, ""); 15516 list.appendChild(item); 15517 }); 15518 15519 target.appendChild(list); 15520 } 15521 15522 function getResultCopy(config, key) { 15523 var resultKeyMap = { 15524 both: "result_both", 15525 financingOnly: "result_ok_fin", 15526 certificationOnly: "result_ok_cer", 15527 none: "result_none" 15528 }; 15529 15530 var fromConfig = null; 15531 15532 if (config && config.results) { 15533 fromConfig = 15534 config.results[key] || 15535 config.results[resultKeyMap[key]] || 15536 null; 15537 } 15538 15539 var base = DEFAULT_RESULT_COPY[key] || DEFAULT_RESULT_COPY.none; 15540 15541 if (!fromConfig) return base; 15542 15543 return { 15544 title: 15545 fromConfig.title || 15546 fromConfig.heading || 15547 base.title, 15548 text: 15549 fromConfig.text || 15550 fromConfig.body || 15551 base.text, 15552 type: fromConfig.type || base.type, 15553 cta1: 15554 fromConfig.cta1 || 15555 fromConfig.primaryCta || 15556 ( 15557 fromConfig.cta1Label && 15558 fromConfig.cta1Href 15559 ? { 15560 text: fromConfig.cta1Label, 15561 href: fromConfig.cta1Href, 15562 target: fromConfig.cta1Target 15563 } 15564 : null 15565 ), 15566 cta2: 15567 fromConfig.cta2 || 15568 fromConfig.secondaryCta || 15569 ( 15570 fromConfig.cta2Label && 15571 fromConfig.cta2Href 15572 ? { 15573 text: fromConfig.cta2Label, 15574 href: fromConfig.cta2Href, 15575 target: fromConfig.cta2Target 15576 } 15577 : null 15578 ) 15579 }; 15580 } 15581 15582 function setCta(link, cta) { 15583 if (!link) return; 15584 15585 if (!cta || !cta.text || !cta.href) { 15586 link.hidden = true; 15587 link.setAttribute("aria-disabled", "true"); 15588 link.setAttribute("tabindex", "-1"); 15589 link.removeAttribute("href"); 15590 link.removeAttribute("target"); 15591 link.removeAttribute("rel"); 15592 link.textContent = ""; 15593 15594 return; 15595 } 15596 15597 link.hidden = false; 15598 link.removeAttribute("aria-disabled"); 15599 link.removeAttribute("tabindex"); 15600 link.setAttribute("href", cta.href); 15601 15602 if (cta.target) { 15603 link.setAttribute("target", cta.target); 15604 15605 if (cta.target === "_blank") { 15606 link.setAttribute("rel", "noopener noreferrer"); 15607 } 15608 } else { 15609 link.removeAttribute("target"); 15610 link.removeAttribute("rel"); 15611 } 15612 15613 link.textContent = cta.text; 15614 } 15615 15616 function WizardController(root) { 15617 this.root = root; 15618 this.config = parseJsonConfig(root) || {}; 15619 this.instanceId = "enisa-wizard-" + (++instanceCount); 15620 15621 var rawQuestions = findQuestionArray(this.config); 15622 15623 this.questions = rawQuestions 15624 .map(normalizeQuestion) 15625 .filter(function (question) { 15626 return ( 15627 question && 15628 question.title && 15629 question.options && 15630 question.options.length 15631 ); 15632 }); 15633 15634 this.answers = []; 15635 this.currentIndex = 0; 15636 15637 this.mainTitle = root.querySelector( 15638 "[data-wizard-main-title]" 15639 ); 15640 this.status = root.querySelector( 15641 "[data-wizard-status]" 15642 ); 15643 this.progress = root.querySelector( 15644 "[data-wizard-progress-fill]" 15645 ); 15646 this.progressWrap = root.querySelector( 15647 ".enisa-wizard__progress[role='progressbar']" 15648 ); 15649 this.stage = root.querySelector( 15650 "[data-wizard-stage]" 15651 ); 15652 this.kicker = root.querySelector( 15653 "[data-wizard-kicker]" 15654 ); 15655 this.question = root.querySelector( 15656 "[data-wizard-question]" 15657 ); 15658 this.hint = root.querySelector( 15659 "[data-wizard-hint]" 15660 ); 15661 this.options = root.querySelector( 15662 "[data-wizard-options]" 15663 ); 15664 this.backBtn = root.querySelector( 15665 "[data-wizard-back]" 15666 ); 15667 this.nextBtn = root.querySelector( 15668 "[data-wizard-next]" 15669 ); 15670 this.resultWrap = root.querySelector( 15671 "[data-wizard-result]" 15672 ); 15673 this.resultIcon = root.querySelector( 15674 "[data-result-icon]" 15675 ); 15676 this.resultTitle = root.querySelector( 15677 "[data-result-title]" 15678 ); 15679 this.resultText = root.querySelector( 15680 "[data-result-text]" 15681 ); 15682 this.resultCta1 = root.querySelector( 15683 "[data-result-cta-1]" 15684 ); 15685 this.resultCta2 = root.querySelector( 15686 "[data-result-cta-2]" 15687 ); 15688 } 15689 15690 WizardController.prototype.init = function () { 15691 if ( 15692 !this.stage || 15693 !this.options || 15694 !this.nextBtn || 15695 !this.backBtn || 15696 !this.question || 15697 !this.kicker 15698 ) { 15699 return; 15700 } 15701 15702 this.scopeStaticIds(); 15703 15704 if (!this.questions.length) { 15705 this.question.textContent = 15706 "No se han podido cargar las preguntas."; 15707 this.kicker.textContent = ""; 15708 this.options.innerHTML = ""; 15709 this.nextBtn.disabled = true; 15710 this.announce( 15711 "No se han podido cargar las preguntas del test." 15712 ); 15713 15714 return; 15715 } 15716 15717 if (this.questions.length < 5) { 15718 this.question.textContent = 15719 "No se han podido cargar todas las preguntas necesarias.";
15720 this.kicker.textContent = ""; 15721 this.options.innerHTML = ""; 15722 this.nextBtn.disabled = true; 15723 this.announce( 15724 "No se han podido cargar todas las preguntas necesarias del test." 15725 ); 15726 15727 return; 15728 } 15729 15730 var self = this; 15731 15732 this.backBtn.addEventListener("click", function () { 15733 self.goBack(); 15734 }); 15735 15736 this.nextBtn.addEventListener("click", function () { 15737 self.goNext(); 15738 }); 15739 15740 this.options.addEventListener("change", function (event) { 15741 var input = event.target; 15742 15743 if ( 15744 !input || 15745 !input.classList || 15746 !input.classList.contains( 15747 "enisa-wizard__option-input" 15748 ) 15749 ) { 15750 return; 15751 } 15752 15753 self.selectOption( 15754 parseInt( 15755 input.getAttribute("data-option-index"), 15756 10 15757 ), 15758 true 15759 ); 15760 }); 15761 15762 this.renderQuestion(false); 15763 }; 15764 15765 WizardController.prototype.scopeStaticIds = function () { 15766 var titleId = setId( 15767 this.mainTitle, 15768 this.instanceId + "-main-title" 15769 ); 15770 var questionId = setId( 15771 this.question, 15772 this.instanceId + "-question" 15773 ); 15774 var hintId = setId( 15775 this.hint, 15776 this.instanceId + "-hint" 15777 ); 15778 var resultTitleId = setId( 15779 this.resultTitle, 15780 this.instanceId + "-result-title" 15781 ); 15782 15783 if (titleId) { 15784 this.root.setAttribute( 15785 "aria-labelledby", 15786 titleId 15787 ); 15788 } 15789 15790 if (questionId) { 15791 this.options.setAttribute( 15792 "aria-labelledby", 15793 questionId 15794 ); 15795 } 15796 15797 if (hintId) { 15798 this.options.setAttribute( 15799 "aria-describedby", 15800 hintId 15801 ); 15802 } 15803 15804 if (resultTitleId && this.resultWrap) { 15805 this.resultWrap.setAttribute( 15806 "aria-labelledby", 15807 resultTitleId 15808 ); 15809 } 15810 15811 this.question.setAttribute("tabindex", "-1"); 15812 15813 if (this.resultTitle) { 15814 this.resultTitle.setAttribute( 15815 "tabindex", 15816 "-1" 15817 ); 15818 } 15819 }; 15820 15821 WizardController.prototype.announce = function (message) { 15822 if (!this.status) return; 15823 15824 this.status.textContent = ""; 15825 15826 var self = this; 15827 15828 window.setTimeout(function () { 15829 self.status.textContent = message || ""; 15830 }, 20); 15831 }; 15832 15833 WizardController.prototype.getProgress = function () { 15834 if (!this.questions.length) return 0; 15835 15836 return Math.round( 15837 ( 15838 (this.currentIndex + 1) / 15839 this.questions.length 15840 ) * 100 15841 ); 15842 }; 15843 15844 WizardController.prototype.getProgressText = function () { 15845 return ( 15846 "Pregunta " + 15847 (this.currentIndex + 1) + 15848 " de " + 15849 this.questions.length 15850 ); 15851 }; 15852 15853 WizardController.prototype.renderQuestion = function ( 15854 moveFocus 15855 ) { 15856 var question = this.questions[this.currentIndex]; 15857 15858 if (!question) return; 15859 15860 var total = this.questions.length; 15861 var selected = this.answers[this.currentIndex]; 15862 15863 this.kicker.textContent = ( 15864 question.kicker || 15865 this.getProgressText() 15866 ) 15867 .replace( 15868 "{index}", 15869 String(this.currentIndex + 1) 15870 ) 15871 .replace( 15872 "{total}", 15873 String(total) 15874 ); 15875 15876 this.question.textContent = question.title; 15877 15878 if (question.hint) { 15879 this.hint.hidden = false; 15880 setHintContent(this.hint, question.hint); 15881 this.options.setAttribute( 15882 "aria-describedby", 15883 this.hint.id 15884 ); 15885 } else { 15886 this.hint.hidden = true; 15887 this.hint.textContent = ""; 15888 this.options.removeAttribute( 15889 "aria-describedby" 15890 ); 15891 } 15892 15893 this.renderOptions(question, selected); 15894 this.syncProgress(); 15895 this.syncNav(); 15896 15897 if (moveFocus) { 15898 this.announce( 15899 this.getProgressText() + 15900 ". " + 15901 question.title 15902 ); 15903 focusElement(this.question); 15904 } 15905 }; 15906 15907 WizardController.prototype.renderOptions = function ( 15908 question, 15909 selected 15910 ) { 15911 var self = this; 15912 var groupName = 15913 this.instanceId + 15914 "-q-" + 15915 makeSafeId( 15916 question.id || 15917 this.currentIndex 15918 ); 15919 15920 this.options.innerHTML = ""; 15921 this.options.setAttribute( 15922 "data-current-question", 15923 String(this.currentIndex) 15924 ); 15925 15926 question.options.forEach(function ( 15927 option, 15928 index 15929 ) { 15930 var optionId = 15931 self.instanceId + 15932 "-q-" + 15933 self.currentIndex + 15934 "-option-" + 15935 index; 15936 15937 var item = document.createElement("div"); 15938 var input = document.createElement("input"); 15939 var label = document.createElement("label"); 15940 var text = document.createElement("span"); 15941 var icon = document.createElement("span"); 15942 var isSelected = selected === index; 15943 15944 item.className = 15945 "enisa-wizard__option-item"; 15946 15947 input.type = "radio"; 15948 input.className = 15949 "enisa-wizard__option-input"; 15950 input.name = groupName; 15951 input.id = optionId; 15952 input.value = option.id; 15953 input.checked = isSelected; 15954 input.setAttribute( 15955 "data-option-index", 15956 String(index) 15957 ); 15958 15959 label.className = 15960 "enisa-wizard__option"; 15961 label.setAttribute("for", optionId); 15962 label.setAttribute( 15963 "data-option-index", 15964 String(index) 15965 ); 15966 15967 if (isSelected) { 15968 item.classList.add("is-selected"); 15969 label.classList.add("is-selected"); 15970 } 15971 15972 text.className = 15973 "enisa-wizard__option-text"; 15974 text.textContent = option.text; 15975 15976 icon.className = 15977 "enisa-wizard__option-icon"; 15978 icon.setAttribute( 15979 "aria-hidden", 15980 "true" 15981 ); 15982 icon.innerHTML = 15983 "<span class='enisa-icon enisa-icon--arrow-right'></span>"; 15984 15985 label.appendChild(text); 15986 label.appendChild(icon); 15987 item.appendChild(input); 15988 item.appendChild(label); 15989 self.options.appendChild(item); 15990 }); 15991 }; 15992 15993 WizardController.prototype.selectOption = function ( 15994 index, 15995 shouldAnnounce 15996 ) { 15997 var question = this.questions[this.currentIndex]; 15998 15999 if ( 16000 !question || 16001 index < 0 || 16002 index >= question.options.length 16003 ) { 16004 return; 16005 } 16006 16007 this.answers[this.currentIndex] = index; 16008 this.syncOptionSelection(); 16009 this.syncNav(); 16010 16011 if (shouldAnnounce) { 16012 this.announce( 16013 "Seleccionado: " + 16014 question.options[index].text + 16015 ". " + 16016 this.getProgressText() + 16017 "." 16018 ); 16019 } 16020 }; 16021 16022 WizardController.prototype.syncOptionSelection = function () { 16023 var selected = this.answers[this.currentIndex]; 16024 var items = this.options.querySelectorAll( 16025 ".enisa-wizard__option-item" 16026 ); 16027 16028 Array.prototype.forEach.call( 16029 items, 16030 function (item) { 16031 var input = item.querySelector( 16032 ".enisa-wizard__option-input" 16033 ); 16034 var label = item.querySelector( 16035 ".enisa-wizard__option" 16036 ); 16037 var index = input 16038 ? parseInt( 16039 input.getAttribute( 16040 "data-option-index" 16041 ), 16042 10 16043 ) 16044 : -1; 16045 var isSelected = index === selected; 16046 16047 item.classList.toggle( 16048 "is-selected", 16049 isSelected 16050 ); 16051 16052 if (label) { 16053 label.classList.toggle( 16054 "is-selected", 16055 isSelected 16056 ); 16057 } 16058 16059 if (input) { 16060 input.checked = isSelected; 16061 } 16062 } 16063 ); 16064 }; 16065 16066 WizardController.prototype.syncProgress = function () { 16067 var percent = this.getProgress(); 16068 var progressText = this.getProgressText(); 16069 16070 if (this.progress) { 16071 this.progress.style.width = 16072 percent + "%"; 16073 } 16074 16075 if (this.progressWrap) { 16076 this.progressWrap.setAttribute( 16077 "aria-valuenow", 16078 String(percent) 16079 ); 16080 this.progressWrap.setAttribute( 16081 "aria-valuetext", 16082 progressText 16083 ); 16084 } 16085 }; 16086 16087 WizardController.prototype.syncNav = function () { 16088 var nextLabel = 16089 this.currentIndex === 16090 this.questions.length - 1 16091 ? "Ver resultado" 16092 : "Continuar"; 16093 16094 this.backBtn.disabled = 16095 this.currentIndex === 0; 16096 16097 this.nextBtn.disabled = 16098 this.answers[this.currentIndex] === 16099 undefined; 16100 16101 this.nextBtn.textContent = nextLabel; 16102 16103 this.nextBtn.removeAttribute( 16104 "aria-label" 16105 ); 16106 }; 16107 16108 WizardController.prototype.goBack = function () { 16109 if (this.currentIndex === 0) return; 16110 16111 this.currentIndex -= 1; 16112 this.renderQuestion(true); 16113 }; 16114 16115 WizardController.prototype.goNext = function () { 16116 if ( 16117 this.answers[this.currentIndex] === 16118 undefined 16119 ) { 16120 return; 16121 } 16122 16123 if ( 16124 this.currentIndex >= 16125 this.questions.length - 1 16126 ) { 16127 this.showResult(); 16128 return; 16129 } 16130 16131 this.currentIndex += 1; 16132 this.renderQuestion(true); 16133 }; 16134 16135 WizardController.prototype.answerIsYes = function ( 16136 questionIndex 16137 ) { 16138 var question = 16139 this.questions[questionIndex]; 16140 var answerIndex = 16141 this.answers[questionIndex]; 16142 16143 if ( 16144 !question || 16145 answerIndex === undefined || 16146 !question.options[answerIndex] 16147 ) { 16148 return false;
16149 } 16150 16151 var option = 16152 question.options[answerIndex]; 16153 16154 return ( 16155 isYesValue(option.value) || 16156 isYesValue(option.id) || 16157 isYesValue(option.text) 16158 ); 16159 }; 16160 16161 WizardController.prototype.computeResultKey = function () { 16162 var q1Yes = this.answerIsYes(0); 16163 var q2Yes = this.answerIsYes(1); 16164 var q3Yes = this.answerIsYes(2); 16165 var q4Yes = this.answerIsYes(3); 16166 var q5Yes = this.answerIsYes(4); 16167 16168 var financiable = 16169 q1Yes && 16170 q4Yes && 16171 q5Yes && 16172 !q3Yes; 16173 16174 var certificable = 16175 q1Yes && 16176 q2Yes && 16177 q5Yes; 16178 16179 if (financiable && certificable) { 16180 return "both"; 16181 } 16182 16183 if (financiable) { 16184 return "financingOnly"; 16185 } 16186 16187 if (certificable) { 16188 return "certificationOnly"; 16189 } 16190 16191 return "none"; 16192 }; 16193 16194 WizardController.prototype.showResult = function () { 16195 var key = this.computeResultKey(); 16196 var result = getResultCopy( 16197 this.config, 16198 key 16199 ); 16200 16201 if (this.resultWrap) { 16202 this.resultWrap.hidden = false; 16203 } 16204 16205 if (this.resultIcon) { 16206 this.resultIcon.setAttribute( 16207 "data-type", 16208 result.type === "ko" 16209 ? "ko" 16210 : "ok" 16211 ); 16212 } 16213 16214 if (this.resultTitle) { 16215 this.resultTitle.textContent = 16216 result.title; 16217 } 16218 16219 if (this.resultText) { 16220 this.resultText.textContent = 16221 result.text; 16222 } 16223 16224 setCta( 16225 this.resultCta1, 16226 result.cta1 16227 ); 16228 setCta( 16229 this.resultCta2, 16230 result.cta2 16231 ); 16232 16233 if (this.resultTitle) { 16234 if ( 16235 window.ENISA && 16236 window.ENISA.utils && 16237 typeof window.ENISA.utils 16238 .focusWithoutScroll === "function" 16239 ) { 16240 window.ENISA.utils.focusWithoutScroll( 16241 this.resultTitle 16242 ); 16243 } else { 16244 focusElement(this.resultTitle); 16245 } 16246 } 16247 16248 if (this.stage) { 16249 this.stage.hidden = true; 16250 } 16251 16252 if (this.progress) { 16253 this.progress.style.width = "100%"; 16254 } 16255 16256 if (this.progressWrap) { 16257 this.progressWrap.setAttribute( 16258 "aria-valuenow", 16259 "100" 16260 ); 16261 this.progressWrap.setAttribute( 16262 "aria-valuetext", 16263 "Test finalizado" 16264 ); 16265 } 16266 16267 this.announce( 16268 "Resultado del test. " + 16269 result.title + 16270 ". " + 16271 result.text 16272 ); 16273 }; 16274 16275 function initEligibilityWizard() { 16276 var roots = 16277 document.querySelectorAll( 16278 ROOT_SELECTOR 16279 ); 16280 16281 if (!roots.length) return; 16282 16283 Array.prototype.forEach.call( 16284 roots, 16285 function (root) { 16286 if ( 16287 root.getAttribute( 16288 "data-wizard-init" 16289 ) === "true" 16290 ) { 16291 return; 16292 } 16293 16294 root.setAttribute( 16295 "data-wizard-init", 16296 "true" 16297 ); 16298 16299 new WizardController(root).init(); 16300 } 16301 ); 16302 } 16303 16304 ready(initEligibilityWizard); 16305 16306 if ("MutationObserver" in window) { 16307 var observerTimer = null; 16308 16309 var observer = new MutationObserver( 16310 function (mutations) { 16311 var hasWizard = mutations.some( 16312 function (mutation) { 16313 return Array.prototype.some.call( 16314 mutation.addedNodes, 16315 function (node) { 16316 if ( 16317 !node || 16318 node.nodeType !== 1 16319 ) { 16320 return false; 16321 } 16322 16323 return ( 16324 node.matches && 16325 node.matches( 16326 ROOT_SELECTOR 16327 ) 16328 ) || ( 16329 node.querySelector && 16330 node.querySelector( 16331 ROOT_SELECTOR 16332 ) 16333 ); 16334 } 16335 ); 16336 } 16337 ); 16338 16339 if (!hasWizard) return; 16340 16341 window.clearTimeout( 16342 observerTimer 16343 ); 16344 16345 observerTimer = 16346 window.setTimeout( 16347 initEligibilityWizard, 16348 150 16349 ); 16350 } 16351 ); 16352 16353 observer.observe(document.body, { 16354 childList: true, 16355 subtree: true 16356 }); 16357 } 16358})(); 16359 16360/* 16361 ENISA | JS Module 16362 Modulo: 31-accessibility-audit 16363 Descripcion: Normalizacion global de nombres accesibles, idioma y contenido dinamico. 16364*/ 16365(function () { 16366 "use strict"; 16367 16368 var INIT_ATTR = "data-enisa-a11y-init"; 16369 var NEW_WINDOW_TEXT = "se abre en una nueva ventana"; 16370 var NEW_WINDOW_HINT = " (" + NEW_WINDOW_TEXT + ")"; 16371 var GENERIC_LABELS = { 16372 "abrir": true, 16373 "continuar": true, 16374 "descargar": true, 16375 "enviar": true, 16376 "leer mas": true, 16377 "leer más": true, 16378 "mas informacion": true, 16379 "mas información": true, 16380 "más informacion": true, 16381 "más información": true, 16382 "mostrar mas": true, 16383 "mostrar más": true, 16384 "ver mas": true, 16385 "ver más": true, 16386 "ver todos": true, 16387 "ver todas": true 16388 }; 16389 16390 function toArray(list) { 16391 return Array.prototype.slice.call(list || []); 16392 } 16393 16394 function normalizeSpaces(value) { 16395 return String(value || "").replace(/\s+/g, " ").trim(); 16396 } 16397 16398 function normalizeText(value) { 16399 return normalizeSpaces(value) 16400 .toLowerCase() 16401 .replace(/[áà äâ]/g, "a") 16402 .replace(/[éèëê]/g, "e") 16403 .replace(/[Ãìïî]/g, "i") 16404 .replace(/[óòöô]/g, "o") 16405 .replace(/[úùüû]/g, "u") 16406 .replace(/ñ/g, "n"); 16407 } 16408 16409 function titleCase(value) { 16410 var text = normalizeSpaces(value); 16411 return text ? text.charAt(0).toUpperCase() + text.slice(1) : ""; 16412 } 16413 16414 function ensureSpanishLanguage(scope) { 16415 var html = document.documentElement; 16416 16417 if (html && normalizeText(html.getAttribute("lang")) !== "es-es") { 16418 html.setAttribute("lang", "es-ES"); 16419 } 16420 16421 if (document.body && !document.body.hasAttribute("lang")) { 16422 document.body.setAttribute("lang", "es-ES"); 16423 } 16424 16425 toArray((scope || document).querySelectorAll("[data-force-spanish]")).forEac
16425h(function (node) { 16426 if (!node.hasAttribute("lang")) { 16427 node.setAttribute("lang", "es-ES"); 16428 } 16429 }); 16430 } 16431 16432 function mergeRel(link) { 16433 var values = normalizeSpaces(link.getAttribute("rel")) 16434 .toLowerCase() 16435 .split(" ") 16436 .filter(Boolean); 16437 16438 if (values.indexOf("noopener") === -1) values.push("noopener"); 16439 if (values.indexOf("noreferrer") === -1) values.push("noreferrer"); 16440 16441 link.setAttribute("rel", values.join(" ")); 16442 } 16443 16444 function hasNewWindowText(value) { 16445 var text = normalizeText(value); 16446 return ( 16447 text.indexOf("nueva ventana") !== -1 || 16448 text.indexOf("nueva pestana") !== -1 || 16449 text.indexOf("pestana nueva") !== -1 16450 ); 16451 } 16452 16453 function normalizeNewWindowText(value) { 16454 return normalizeSpaces(value) 16455 .replace(/se abre en una nueva ventana/gi, NEW_WINDOW_TEXT) 16456 .replace(/se abre en una nueva pestaña/gi, NEW_WINDOW_TEXT) 16457 .replace(/abre en una nueva pestaña/gi, NEW_WINDOW_TEXT) 16458 .replace(/abre en nueva pestaña/gi, NEW_WINDOW_TEXT) 16459 .replace(/se abre en pestaña nueva/gi, NEW_WINDOW_TEXT) 16460 .replace(/abre en pestaña nueva/gi, NEW_WINDOW_TEXT) 16461 .replace(/se abre en una pestaña nueva/gi, NEW_WINDOW_TEXT); 16462 } 16463 16464 function getVisibleText(element) { 16465 if (!element) return ""; 16466 16467 return normalizeSpaces( 16468 toArray(element.childNodes) 16469 .filter(function (node) { 16470 return node.nodeType === 3 || 16471 (node.nodeType === 1 && node.getAttribute("aria-hidden") !== "true"); 16472 }) 16473 .map(function (node) { 16474 return node.textContent || ""; 16475 }) 16476 .join(" ") 16477 ); 16478 } 16479 16480 function appendNewWindowHint(link) { 16481 var existing = link.querySelector("[data-new-tab-hint], [data-new-window-hint]"); 16482 16483 if (existing) { 16484 existing.setAttribute("data-new-window-hint", ""); 16485 existing.className = existing.className || "enisa-sr-only"; 16486 existing.textContent = NEW_WINDOW_HINT; 16487 toArray(link.querySelectorAll("[data-new-tab-hint], [data-new-window-hint]")).forEach(function (hint) { 16488 if (hint !== existing) hint.parentNode.removeChild(hint); 16489 }); 16490 return; 16491 } 16492 16493 var hint = document.createElement("span"); 16494 hint.className = "enisa-sr-only"; 16495 hint.setAttribute("data-new-window-hint", ""); 16496 hint.textContent = NEW_WINDOW_HINT; 16497 link.appendChild(hint); 16498 } 16499 16500 function enhanceNewWindowLink(link) { 16501 var ariaLabel = normalizeSpaces(link.getAttribute("aria-label")); 16502 var title = normalizeSpaces(link.getAttribute("title")); 16503 16504 mergeRel(link); 16505 16506 if (ariaLabel) { 16507 ariaLabel = normalizeNewWindowText(ariaLabel); 16508 if (!hasNewWindowText(ariaLabel)) { 16509 ariaLabel += NEW_WINDOW_HINT; 16510 } 16511 link.setAttribute("aria-label", ariaLabel); 16512 toArray(link.querySelectorAll("[data-new-tab-hint], [data-new-window-hint]")).forEach(function (hint) { 16513 hint.parentNode.removeChild(hint); 16514 }); 16515 return; 16516 } 16517 16518 if (title) { 16519 title = normalizeNewWindowText(title); 16520 if (!hasNewWindowText(title)) { 16521 title += NEW_WINDOW_HINT; 16522 } 16523 link.setAttribute("title", title); 16524 } 16525 16526 appendNewWindowHint(link); 16527 } 16528 16529 function getControlImageText(control) { 16530 var image = control.querySelector("img[alt], svg[aria-label], svg title"); 16531 16532 if (!image) return ""; 16533 if (image.tagName && image.tagName.toLowerCase() === "title") { 16534 return normalizeSpaces(image.textContent); 16535 } 16536 16537 return normalizeSpaces(image.getAttribute("alt") || image.getAttribute("aria-label")); 16538 } 16539 16540 function inferIconOnlyControlLabel(control) { 16541 var title = normalizeSpaces(control.getAttribute("title")); 16542 var className = normalizeText(control.className || ""); 16543 var controlRef = normalizeText(control.getAttribute("aria-controls")); 16544 var type = normalizeText(control.getAttribute("type")); 16545 var imageText = getControlImageText(control); 16546 16547 if (title) return title; 16548 if (imageText) return imageText; 16549 if (className.indexOf("search") !== -1 || controlRef.indexOf("search") !== -1) return "Buscar"; 16550 if (className.indexOf("clear") !== -1) return "Borrar"; 16551 if (className.indexOf("close") !== -1) return "Cerrar";
16552 if (className.indexOf("menu") !== -1 || className.indexOf("toggle") !== -1) return "Abrir menú"; 16553 if (className.indexOf("share") !== -1) return "Compartir"; 16554 if (className.indexOf("download") !== -1) return "Descargar"; 16555 if (className.indexOf("private") !== -1 || className.indexOf("user") !== -1) return "Ãrea privada"; 16556 if (className.indexOf("prev") !== -1 || className.indexOf("previous") !== -1) return "Anterior"; 16557 if (className.indexOf("next") !== -1) return "Siguiente"; 16558 if (type === "submit") return "Enviar"; 16559 16560 return ""; 16561 } 16562 16563 function ensureIconOnlyControlLabels(scope) { 16564 toArray((scope || document).querySelectorAll("a[href], button")).forEach(function (control) { 16565 var label = normalizeSpaces(control.getAttribute("aria-label")); 16566 var labelledBy = normalizeSpaces(control.getAttribute("aria-labelledby")); 16567 var visible = getVisibleText(control); 16568 var inferred; 16569 16570 if (label || labelledBy || visible) return; 16571 16572 inferred = inferIconOnlyControlLabel(control); 16573 if (!inferred) return; 16574 16575 if (control.matches("a[target='_blank']") && !hasNewWindowText(inferred)) { 16576 inferred += NEW_WINDOW_HINT; 16577 } 16578 16579 control.setAttribute("aria-label", normalizeSpaces(inferred)); 16580 }); 16581 } 16582 16583 function getClosestContext(control) { 16584 var root = control.closest( 16585 "article, li, section, form, [data-card-link], [data-news-card], [data-resource-item], [data-transparency-row], .enisa-news-card, .enisa-editorial-card, .enisa-cards-recursos__card, .enisa-recursos-detalle__item, .enisa-transparencia-documents__row" 16586 ); 16587 var selectors = [ 16588 "[data-accessible-title]", 16589 "[data-search-title]", 16590 ".enisa-news-card__title", 16591 ".enisa-editorial-latest-card__title", 16592 ".enisa-cards-recursos__card-title", 16593 ".enisa-recursos-detalle__title", 16594 ".enisa-transparencia-documents__cell:first-child", 16595 ".enisa-transparencia-documents-card__title", 16596 ".enisa-accordion__download-label", 16597 ".enisa-materiales__download-label", 16598 ".enisa-btn__label", 16599 "h1, h2, h3, h4, h5, h6" 16600 ]; 16601 var node; 16602 16603 if (root) { 16604 for (var i = 0; i < selectors.length; i += 1) { 16605 node = root.querySelector(selectors[i]); 16606 if (node && node !== control) { 16607 var text = normalizeSpaces(node.getAttribute("data-accessible-title") || node.textContent); 16608 if (text && text !== getVisibleText(control)) return text; 16609 } 16610 } 16611 } 16612 16613 node = control.closest("section"); 16614 if (node) { 16615 var labelledBy = node.getAttribute("aria-labelledby"); 16616 var heading = labelledBy ? document.getElementById(labelledBy) : node.querySelector("h1, h2, h3"); 16617 if (heading) return normalizeSpaces(heading.textContent); 16618 } 16619 16620 return ""; 16621 } 16622 16623 function getDownloadContext(control) { 16624 var downloadLabel = control.querySelector( 16625 ".enisa-accordion__download-label, .enisa-materiales__download-label, .enisa-btn__label" 16626 ); 16627 return normalizeSpaces(downloadLabel ? downloadLabel.textContent : "") || getClosestContext(control); 16628 } 16629 16630 function isGenericControl(control) { 16631 var text = normalizeText(getVisibleText(control)); 16632 if (!text) return false; 16633 return GENERIC_LABELS[text] === true; 16634 } 16635 16636 function enhanceGenericControl(control) { 16637 if (!isGenericControl(control)) return; 16638 if (normalizeSpaces(control.getAttribute("aria-label"))) return; 16639 16640 var visible = normalizeSpaces(getVisibleText(control)); 16641 var normalized = normalizeText(visible); 16642 var context = normalized === "descargar" ? getDownloadContext(control) : getClosestContext(control); 16643 var label = titleCase(visible); 16644 16645 if (context && normalizeText(context) !== normalizeText(visible)) { 16646 if (normalized.indexOf("descargar") === 0) { 16647 label = "Descargar " + context; 16648 } else if (normalized.indexOf("enviar") === 0) { 16649 label = "Enviar formulario" + (context ? " de " + context : ""); 16650 } else { 16651 label = visible + ": " + context; 16652 } 16653 } 16654 16655 if (control.matches("a[target='_blank']") && !hasNewWindowText(label)) { 16656 label += NEW_WINDOW_HINT; 16657 } 16658 16659 control.setAttribute("aria-label", normalizeSpaces(label)); 16660 } 16661 16662 function hasSvgAccessibleName(svg) { 16663 return !!( 16664 normalizeSpaces(svg.getAttribute("aria-label")) || 16665 normalizeSpaces(svg.getAttribute("aria-labelledby")) || 16666 svg.querySelector("title") 16667 ); 16668 } 16669 16670 function silenceDecorativeIcons(scope) { 16671 toArray((scope || document).querySelectorAll("a img, button img, a svg, button svg, .enisa-icon, [class*='__icon'] svg, svg")).forEac
16671h(function (icon) { 16672 if (icon.closest("[data-icon-informative]")) return; 16673 if (icon.tagName && icon.tagName.toLowerCase() === "svg" && hasSvgAccessibleName(icon)) return; 16674 16675 icon.setAttribute("aria-hidden", "true"); 16676 16677 if (icon.tagName && icon.tagName.toLowerCase() === "img") { 16678 icon.setAttribute("alt", ""); 16679 } 16680 16681 if (icon.tagName && icon.tagName.toLowerCase() === "svg") { 16682 icon.setAttribute("focusable", "false"); 16683 } 16684 }); 16685 } 16686 16687 function ensureImageAlts(scope) { 16688 toArray((scope || document).querySelectorAll("img")).forEach(function (image) { 16689 var context = getClosestContext(image) || image.getAttribute("title") || ""; 16690 16691 if (image.closest("a, button") || image.closest("[aria-hidden='true']")) { 16692 image.setAttribute("alt", ""); 16693 image.setAttribute("aria-hidden", "true"); 16694 return; 16695 } 16696 16697 if (image.hasAttribute("alt") && normalizeSpaces(image.getAttribute("alt")) === "") { 16698 image.setAttribute("aria-hidden", "true"); 16699 return; 16700 } 16701 16702 if (image.hasAttribute("alt")) return; 16703 16704 image.setAttribute("alt", context ? context : ""); 16705 if (!context) image.setAttribute("aria-hidden", "true"); 16706 }); 16707 } 16708 16709 function normalizeDuplicateCardLinks(scope) { 16710 toArray((scope || document).querySelectorAll("a[tabindex='-1'][href]")).forEach(function (link) { 16711 var card = link.closest("[data-card-link]"); 16712 var primaryTarget = card ? card.querySelector("[data-card-link-target][href]") : null; 16713 16714 if (!link.matches("[class*='image-wrapper'], [class*='media-link'], [class*='image-link']")) return; 16715 if (!primaryTarget || primaryTarget === link) return; 16716 16717 link.removeAttribute("href"); 16718 link.removeAttribute("aria-label"); 16719 link.setAttribute("role", "presentation"); 16720 toArray(link.querySelectorAll("img, svg")).forEach(function (media) { 16721 media.setAttribute("aria-hidden", "true"); 16722 if (media.tagName && media.tagName.toLowerCase() === "img") media.setAttribute("alt", ""); 16723 if (media.tagName && media.tagName.toLowerCase() === "svg") media.setAttribute("focusable", "false"); 16724 }); 16725 }); 16726 } 16727 16728 function ensureLandmarkBasics(scope) { 16729 var root = scope && scope.querySelectorAll ? scope : document; 16730 var main = document.getElementById("main-content"); 16731 var skip = document.querySelector(".enisa-skip-link[href^='#']"); 16732 16733 if (!main) { 16734 main = document.querySelector("main.enisa-main, main.page-layout__main, main"); 16735 if (main && !main.id) { 16736 main.id = "main-content"; 16737 } 16738 } 16739 16740 if (main) { 16741 if (!main.hasAttribute("tabindex")) { 16742 main.setAttribute("tabindex", "-1"); 16743 } 16744 16745 if (!main.hasAttribute("lang")) { 16746 main.setAttribute("lang", "es"); 16747 } 16748 } 16749 16750 if (skip && main) { 16751 skip.setAttribute("href", "#main-content"); 16752 } 16753 16754 toArray(root.querySelectorAll("[role='main']")).forEach(function (landmark) { 16755 if (landmark.tagName && landmark.tagName.toLowerCase() === "main") { 16756 landmark.removeAttribute("role"); 16757 } 16758 }); 16759 } 16760 16761 function isHiddenForAudit(element) { 16762 var style; 16763 16764 if (!element) return true; 16765 if (element.hidden || element.getAttribute("aria-hidden") === "true") return true; 16766 if (element.closest("[hidden], [aria-hidden='true'], [inert]")) return true; 16767 16768 if (window.getComputedStyle) { 16769 style = window.getComputedStyle(element); 16770 if (style.display === "none" || style.visibility === "hidden") return true; 16771 } 16772 16773 return false; 16774 } 16775 16776 function auditStructure(scope) { 16777 var root = scope && scope.querySelectorAll ? scope : document; 16778 var main = document.getElementById("main-content"); 16779 var skip = document.querySelector(".enisa-skip-link"); 16780 var header = document.querySelector("[data-header-root], .enisa-header, body > header"); 16781 var footer = document.querySelector("footer, .enisa-footer"); 16782 var landmarks = toArray(document.querySelectorAll("main, [role='main']")).map(function (landmark) { 16783 return { 16784 tag: landmark.tagName.toLowerCase(), 16785 id: landmark.id || "", 16786 className: typeof landmark.className === "string" ? landmark.className : "", 16787 hidden: isHiddenForAudit(landmark) 16788 }; 16789 }); 16790 var hiddenFocusable = toArray(root.querySelectorAll("a[href], button, input, select, textarea, summary, [tabindex]")) 16791 .filter(function (element) { 16792 return isHiddenForAudit(element) && element.tabIndex >= 0; 16793 }) 16794 .slice(0, 25) 16795 .map(function (element) { 16796 return { 16797 tag: element.tagName.toLowerCase(),
16798 id: element.id || "", 16799 className: typeof element.className === "string" ? element.className : "", 16800 text: normalizeSpaces(element.textContent).slice(0, 80) 16801 }; 16802 }); 16803 var nestedInteractive = toArray(root.querySelectorAll("a[href], button, [role='button'], [role='link']")) 16804 .filter(function (control) { 16805 return !!control.querySelector("a[href], button, input, select, textarea, [role='button'], [role='link']"); 16806 }) 16807 .slice(0, 25) 16808 .map(function (control) { 16809 return { 16810 tag: control.tagName.toLowerCase(), 16811 id: control.id || "", 16812 className: typeof control.className === "string" ? control.className : "", 16813 text: normalizeSpaces(control.textContent).slice(0, 80) 16814 }; 16815 }); 16816 16817 return { 16818 visibleMainCount: landmarks.filter(function (item) { return !item.hidden; }).length, 16819 landmarks: landmarks, 16820 mainContentExists: !!main, 16821 mainHasProgrammaticFocus: !!(main && main.getAttribute("tabindex") === "-1"), 16822 skipExists: !!skip, 16823 skipHref: skip ? skip.getAttribute("href") : null, 16824 skipTargetsMain: !!(skip && main && skip.getAttribute("href") === "#main-content"), 16825 skipBeforeHeader: !!(skip && header && (skip.compareDocumentPosition(header) & Node.DOCUMENT_POSITION_FOLLOWING)), 16826 mainContainsHeader: !!(main && main.querySelector("[data-header-root], .enisa-header, header, nav")), 16827 mainContainsFooter: !!(main && footer && main.contains(footer)), 16828 bodyHasHeadLeak: !!(document.body && document.body.querySelector("meta, title, head, body")), 16829 hiddenFocusable: hiddenFocusable, 16830 nestedInteractive: nestedInteractive 16831 }; 16832 } 16833 16834 function normalizeRedundantLabels(scope) { 16835 toArray((scope || document).querySelectorAll("a[aria-label], button[aria-label]")).forEach(function (control) { 16836 var label = normalizeSpaces(control.getAttribute("aria-label")); 16837 var visible = getVisibleText(control); 16838 16839 if (!label || !visible) return; 16840 if (hasNewWindowText(label) && !hasNewWindowText(visible)) return; 16841 16842 if (normalizeText(label) === normalizeText(visible)) { 16843 control.removeAttribute("aria-label"); 16844 } 16845 }); 16846 16847 toArray((scope || document).querySelectorAll("article[aria-label], li[aria-label], div[aria-label]")).forEach(function (container) { 16848 var role = normalizeSpaces(container.getAttribute("role")); 16849 var label = normalizeSpaces(container.getAttribute("aria-label")); 16850 var heading = container.querySelector("h1, h2, h3, h4, h5, h6"); 16851 16852 if (role || !label || !heading) return; 16853 if (normalizeText(label) === normalizeText(heading.textContent)) { 16854 container.removeAttribute("aria-label"); 16855 } 16856 }); 16857 } 16858 16859 function normalizeAriaReferences(scope) { 16860 toArray((scope || document).querySelectorAll("[aria-labelledby], [aria-describedby]")).forEach(function (element) { 16861 ["aria-labelledby", "aria-describedby"].forEach(function (attr) { 16862 var raw = normalizeSpaces(element.getAttribute(attr)); 16863 if (!raw) return; 16864 16865 var ids = raw.split(" ").filter(function (id) { 16866 var ref = document.getElementById(id); 16867 if (!ref) return false; 16868 if (attr === "aria-labelledby") return true; 16869 if (!ref.hidden) return true; 16870 return /(error|feedback|hint|status|help|legal)/i.test(id); 16871 }); 16872 16873 if (ids.length) { 16874 element.setAttribute(attr, ids.join(" ")); 16875 } else { 16876 element.removeAttribute(attr); 16877 } 16878 }); 16879 }); 16880 } 16881 16882 function normalizeLiveRegions(scope) { 16883 toArray((scope || document).querySelectorAll("[aria-live]")).forEach(function (node) { 16884 var role = normalizeSpaces(node.getAttribute("role")).toLowerCase(); 16885 16886 if (role === "list" || role === "listbox") { 16887 node.removeAttribute("aria-live"); 16888 node.removeAttribute("aria-atomic"); 16889 } 16890 }); 16891 } 16892 16893 function normalizeStaticTabPanelFocus(scope) { 16894 var root = scope && scope.querySelectorAll ? scope : document; 16895 var selector = "[role='tabpanel'][tabindex='0'], [data-resource-panel][tabindex='0'], [data-panel-index][tabindex='0']"; 16896 var panels = []; 16897 16898 if (root.matches && root.matches(selector)) { 16899 panels.push(root); 16900 } 16901 16902 panels = panels.concat(toArray(root.querySelectorAll(selector))); 16903
16904 panels.forEach(function (panel) { 16905 panel.removeAttribute("tabindex"); 16906 }); 16907 } 16908 16909 function normalizeCarouselControl(button) { 16910 var label = normalizeSpaces(button.getAttribute("aria-label")); 16911 var group = button.closest("[aria-label]"); 16912 var groupLabel = group ? normalizeSpaces(group.getAttribute("aria-label")) : ""; 16913 var total = group ? group.querySelectorAll("button").length : 0; 16914 var index = toArray(group ? group.querySelectorAll("button") : []).indexOf(button) + 1; 16915 16916 if (!label) return; 16917 16918 if (/^tarjeta anterior$/i.test(label) && groupLabel) { 16919 button.setAttribute("aria-label", "Anterior elemento. " + groupLabel); 16920 } else if (/^tarjeta siguiente$/i.test(label) && groupLabel) { 16921 button.setAttribute("aria-label", "Siguiente elemento. " + groupLabel); 16922 } else if (/^pagina \d+$/i.test(normalizeText(label)) && total > 1) { 16923 button.setAttribute("aria-label", "Ir al elemento " + index + " de " + total); 16924 } 16925 } 16926 16927 function enhanceScope(scope) { 16928 var root = scope && scope.querySelectorAll ? scope : document; 16929 var links = []; 16930 var controls = []; 16931 16932 ensureSpanishLanguage(root); 16933 ensureIconOnlyControlLabels(root); 16934 silenceDecorativeIcons(root); 16935 ensureImageAlts(root); 16936 normalizeDuplicateCardLinks(root); 16937 ensureLandmarkBasics(root); 16938 normalizeRedundantLabels(root); 16939 normalizeAriaReferences(root); 16940 normalizeLiveRegions(root); 16941 normalizeStaticTabPanelFocus(root); 16942 16943 if (root.matches && root.matches("a[target='_blank']")) { 16944 links.push(root); 16945 } 16946 16947 links = links.concat(toArray(root.querySelectorAll("a[target='_blank']"))); 16948 links.forEach(enhanceNewWindowLink); 16949 16950 if (root.matches && root.matches("a[href], button")) { 16951 controls.push(root); 16952 } 16953 16954 controls = controls.concat(toArray(root.querySelectorAll("a[href], button"))); 16955 controls.forEach(function (control) { 16956 enhanceGenericControl(control); 16957 normalizeCarouselControl(control); 16958 control.setAttribute(INIT_ATTR, "true"); 16959 }); 16960 } 16961 16962 function init() { 16963 enhanceScope(document); 16964 16965 if (!("MutationObserver" in window) || !document.body) return; 16966 16967 var timer = null; 16968 var observer = new MutationObserver(function (mutations) { 16969 var nodes = []; 16970 16971 mutations.forEach(function (mutation) { 16972 toArray(mutation.addedNodes).forEach(function (node) { 16973 if (node && node.nodeType === 1) nodes.push(node); 16974 }); 16975 }); 16976 16977 if (!nodes.length) return; 16978 16979 window.clearTimeout(timer); 16980 timer = window.setTimeout(function () { 16981 nodes.forEach(enhanceScope); 16982 }, 80); 16983 }); 16984 16985 observer.observe(document.body, { 16986 childList: true, 16987 subtree: true 16988 }); 16989 } 16990 16991 if (document.readyState === "loading") { 16992 document.addEventListener("DOMContentLoaded", init); 16993 } else { 16994 init(); 16995 } 16996 16997 window.ENISA = window.ENISA || {}; 16998 window.ENISA.accessibility = { 16999 refresh: enhanceScope, 17000 audit: auditStructure 17001 }; 17002})(); 17003
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.