1/*1790262248000*/ 2/** 3 * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 4 * 5 * This library is free software; you can redistribute it and/or modify it under 6 * the terms of the GNU Lesser General Public License as published by the Free 7 * Software Foundation; either version 2.1 of the License, or (at your option) 8 * any later version. 9 * 10 * This library is distributed in the hope that it will be useful, but WITHOUT 11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 13 * details. 14 */ 15 16/* 17 * This function gets loaded when all the HTML, not including the portlets, is 18 * loaded. 19 */ 20 21 22AUI().ready(function () { 23 // Accordions 24 initPirhaAccordions(); 25 26 // close menus when clicking ESC 27 window.addEventListener('keydown', (e) => { 28 if (e.key === 'Escape') { 29 hideMenu("all"); 30 } 31 }); 32 33 // Hide menus on media breakpoints 34 const mqlDesktop = window.matchMedia("(max-width: 991px)"); 35 const mqlDesktopLarge = window.matchMedia("(max-width: 1200px)"); 36 37 mqlDesktop.onchange = (e) => { 38 if (!e.matches) { 39 hideMenu('mega'); 40 } 41 if (e.matches) { 42 hideMenu('all'); 43 } 44 }; 45 46 const megaMenuMobile = document.querySelector('#header__mega-menu__mobile'); 47 window.onresize = (function(){ 48 if(mqlDesktopLarge.matches && megaMenuMobile && megaMenuMobile.style.display != 'none'){ 49 setMobileNavigationHeight(); 50 } 51 }); 52 53 const megaMenu = document.querySelector('#header__mega-menu'); 54 document.addEventListener('click', (evt) => { 55 if(megaMenu && !megaMenu.contains(evt.target)) { 56 hideDesktopMegamenu(); 57 } 58 }); 59 60 /* Mobilenav */ 61 mobileNavigation(); 62 63 /* Search suggestions */ 64 customizeSearchSuggestionsLinks(); 65}); 66 67/* 68 * This function gets loaded after each and every portlet on the page. 69 * 70 * portletId: the current portlet's id 71 * node: the Alloy Node object of the current portlet 72 */ 73Liferay.Portlet.ready(function (_portletId, _node) { 74}); 75/* 76 * This function gets loaded when everything, including the portlets, is on 77 * the page. 78 */ 79Liferay.on('allPortletsReady', function () { 80 let pirhaDropdowns = [...document.querySelectorAll('.pirha-dropdown')]; 81 if (pirhaDropdowns.length > 0) { 82 pirhaDropdowns.forEach(dropdown => { 83 const toggler = dropdown.querySelector('.dropdown-toggle'); 84 const content = dropdown.querySelector('.dropdown-menu'); 85 if (toggler && content) { 86 87 toggler.addEventListener('click', () => { 88 toggler.setAttribute('aria-expanded', toggler.getAttribute('aria-expanded') ? 'true' : 'false'); 89 dropdown.classList.toggle('open'); 90 91 setTimeout(() => { 92 if (dropdown.classList.contains('open')) { 93 // Move focus on first link or checkbox 94 const links = [...dropdown.querySelectorAll('a, input[type="checkbox"]')]; 95 96 links[0]?.focus(); 97 } 98 }, 500); 99 }); 100 101 // Close dropdown when clicking outside dropdown 102 document.addEventListener('click', (evt) => { 103 if(!dropdown.contains(evt.target)) { 104 toggler.setAttribute('aria-expanded', 'false'); 105 dropdown.classList.remove('open'); 106 } 107 }); 108 } 109 }); 110 } 111 112 // Mobile search toggler 113 const mobileSearchToggler = document.getElementById('header__top__mobile-search__button'); 114 if (mobileSearchToggler) { 115 const mobileSearchContainer = document.getElementById('header__search__mobile'); 116 const mobileSearchInput = document.querySelector('#header__search__mobile .search-bar-keywords-input'); 117 118 mobileSearchToggler.addEventListener('click', (event) => { 119 mobileSearchToggler.setAttribute('aria-expanded', mobileSearchToggler.getAttribute('aria-expanded') == 'false' ? 'true' : 'false'); 120 121 if(mobileSearchInput && document.activeElement != mobileSearchInput){ 122 setTimeout(() => { 123 mobileSearchInput.focus(); 124 }, 500); 125 } 126 127 if (mobileSearchContainer) { 128 // check if mobile slide menu should be closed 129 const mobileMenuToggler = document.getElementById('header__top__mobile-menu__button'); 130 updateMobileMenuToggler(mobileMenuToggler); 131 mobileSearchContainer.classList.toggle('d-none'); 132 } 133 }); 134 135 // Hide search element when is clicked outside button or search element 136 document.addEventListener('click', (evt) => { 137 if (mobileSearchToggler.getAttribute('aria-expanded') === 'true') { 138 if(!mobileSearchToggler.contains(evt.target) && !mobileSearchContainer.contains(evt.target)) { 139 mobileSearchToggler.setAttribute('aria-expanded', 'false'); 140 mobileSearchContainer.classList.add('d-none'); 141 } 142 } 143 }); 144 } 145 146 /* Open megamenu parent level when lower level page is current page */ 147 openActiveSubmenuParents(); 148 mainNavArrowKey(document.querySelectorAll('#header__mega-menu__bar .mega-menu__top-level__item')); 149 150 /* Detect and Add external link icons to main and footer link elements */ 151 detectExternalLinks('#main-content a'); 152 detectExternalLinks('#footer a'); 153 154 /* Add uimage viewers */ 155 addAUimageViewers(); 156}); 157 158document.querySelectorAll('.portlet').forEach(function (element) { 159 element.addEventListener("mouseover", function () { 160 const isPortletTopper = element.getElementsByClassName("portlet-topper"); 161 if (isPortletTopper) { 162 for (var i = 0; i < isPortletTopper.length; i++) { 163 const isPortletNameText = isPortletTopper[i].getElementsByClassName("portlet-name-text"); 164 if (isPortletNameText) { 165 if (isPortletNameText[0].innerText === "Sisälletyt portletit" || isPortletNameText[0].innerText === "Nested Applications") { 166 isPortletTopper[i].classList.add("nested-application-topper"); 167 } 168 } 169 } 170 } 171 }); 172}); 173 174// Hide portlet toppers from Display Template Pages 175if (window.location.href.indexOf("/w/") > -1) { 176 if (document.getElementsByClassName('portlet-topper') !== null) { 177 const elements = document.getElementsByClassName('portlet-topper'); 178 for (let i = 0; i < elements.length; i++) { 179 elements[i].style.display = "none"; 180 } 181 } 182} 183 184function updateMobileMenuToggler(mobileMenuToggler){ 185 if (mobileMenuToggler && mobileMenuToggler.getAttribute('aria-expanded') === 'true') { 186 mobileMenuToggler.setAttribute('aria-expanded', 'false'); 187 const icon = mobileMenuToggler.querySelector('.pirha-icon'); 188 icon.classList.remove('pirha-icon--close'); 189 icon.classList.add('pirha-icon--menu'); 190 191 document.getElementById('header__mega-menu__mobile').style.display = 'none'; 192 document.querySelector('body').classList.remove('mobile-menu-open'); 193 } 194} 195 196// show mega menu by clicking one of the top level buttons 197function showMegaMenuDropdown(index, isMobile = false) { 198 let mobileDropdown = isMobile ? '_mobile': ''; 199 200 const clickedDropdown = document.getElementById("mega-menu__dropdown"+ mobileDropdown +"--" + index); 201 const clickedButton = clickedDropdown.previousElementSibling; 202
203 /* Set aria-expanded to false by default on Top-level buttons */ 204 document.querySelectorAll('button.mega-menu__top-level__item__content').forEach(button => { 205 if(button !== clickedButton){ 206 button.setAttribute('aria-expanded', 'false'); 207 } 208 }); 209 210 /* on click, toggle aria-expanded attribute */ 211 clickedButton.setAttribute('aria-expanded', clickedButton.getAttribute('aria-expanded') == 'false' ? 'true' : 'false'); 212 const menuIsExpanded = clickedButton.getAttribute('aria-expanded') == 'true'; 213 214 openActiveSubmenuParents(); 215 setDropdownHeight(clickedButton); 216 217 // Array of megamenu panels / sections for keyboard pathing 218 const megamenu = []; 219 megamenu.push([...clickedDropdown.querySelectorAll('.level-0')]); 220 megamenu.push([...clickedDropdown.querySelectorAll('.level-1')]); 221 megamenu.push([...clickedDropdown.querySelectorAll('.level-2')]); 222 megamenu.push([...clickedDropdown.querySelectorAll('.mega-menu__bottom-row .journal-content-article > ul')]); // note: keep bottom-row last 223 224 clickedDropdown.removeEventListener('keydown', keyboardPathing, false); 225 226 if( menuIsExpanded){ 227 clickedDropdown.addEventListener('keydown', keyboardPathing.bind(null, megamenu), false); 228 } 229 230 /* Prevent navigation mini-menu dropdowns to open outside wrapper */ 231 if(clickedDropdown && clickedDropdown.classList.contains('mini-menu__dropdown')) { 232 const offsetLeft = clickedDropdown.getBoundingClientRect().left; 233 const width = clickedDropdown.offsetWidth; 234 const wrapperWidth = document.getElementById('wrapper').offsetWidth; 235 const isEntirelyVisible = (offsetLeft + width <= wrapperWidth); 236 237 if (!isEntirelyVisible) { 238 clickedDropdown.classList.add('position-right'); 239 } 240 } 241} 242 243// Navigate mega menu columns 244const keyboardPathing = function(megamenu, e) { 245 var isTabPressed = (e.key === 'Tab' || e.keyCode === 9); 246 247 if (!isTabPressed) { 248 return; 249 } 250 251 const parentMenuID = document.activeElement.closest('ul').getAttribute('data-parent-id'); 252 const menuLevel = Number(document.activeElement.closest('ul').getAttribute('data-level')); 253 const lastMegamenuList = megamenu[megamenu.length - 1][0]; 254 const hasBottomRowLinks = lastMegamenuList.querySelector('li.mega-menu__dropdown__list__item a') ? true : false; 255 256 /* shift + tab */ 257 if(e.shiftKey) { 258 // if 2nd or 3rd column 259 if(menuLevel > 0){ 260 // Get open column 261 const activeColumn = megamenu[menuLevel].filter(menu => { 262 return menu.classList.contains('open'); 263 }) 264 265 // Move focus back to parent column, when active element is first item 266 if(document.activeElement.closest('li') == activeColumn[0].children[0]){ 267 document.querySelector('.toggle-submenu[data-target-sub-menu="'+ parentMenuID +'"]').focus(); 268 e.preventDefault(); 269 } 270 } 271 else if (hasBottomRowLinks){ 272 if(document.activeElement.closest('li') == lastMegamenuList.children[0]){ 273 const lastLinkOfFirstColumn = megamenu[0][0].querySelector('li.mega-menu__dropdown__list__item:last-child > a'); 274 lastLinkOfFirstColumn?.focus(); 275 e.preventDefault(); 276 } 277 } 278 } 279 /* tab */ 280 else { 281 let submenuLevel = menuLevel + 1; 282 283 if(submenuLevel <= 3){ 284 let activeSubColumn = megamenu[submenuLevel]?.filter(menu => { 285 return menu.classList.contains('open'); 286 }) 287 288 const activeColumn = megamenu[menuLevel].filter(menu => { 289 return menu.classList.contains('open'); 290 }) 291 292 // Move focus to first item of submenu 293 // (navigation column structure is not nested) 294 if(document.activeElement.getAttribute('aria-expanded') == 'true'){ 295 activeSubColumn[0].querySelector('a').focus(); 296 e.preventDefault(); 297 } 298 299 // When current activeElement is not in first column 300 if(menuLevel > 0){
301 // activeELement is Last element in submenu column 302 if(document.activeElement.closest('li') == activeColumn[0].children[activeColumn[0].children.length - 1]){ 303 const nextSibling = document.activeElement.nextElementSibling; 304 let parentColumnLastChild = document.querySelector('.toggle-submenu[data-target-sub-menu="'+ parentMenuID +'"]').parentNode.lastElementChild; 305 let returnLevel = parentMenuID 306 307 if(menuLevel == 2){ 308 parentColumnLastChild = document.querySelector('.toggle-submenu[data-target-sub-menu="'+ parentMenuID +'"]').parentNode.parentNode.lastElementChild; 309 } 310 311 const lastChildSubmenuIsOpen = parentColumnLastChild.querySelector('button.toggle-submenu')?.getAttribute('aria-expanded') == 'true'; 312 313 // Return to first column (level-0) when 3rd column is active 314 // and parent element is last item of 2nd column 315 if(lastChildSubmenuIsOpen){ 316 returnLevel = document.querySelector('.toggle-submenu[data-target-sub-menu="'+ parentMenuID +'"]').parentNode.parentNode.dataset.parentId; 317 } 318 319 const parentNodeEl = document.querySelector('.toggle-submenu[data-target-sub-menu="'+ returnLevel +'"]').parentNode; 320 321 // If active element doesn't have childs or active element is toggle button 322 if((document.activeElement.nodeName == 'A' && nextSibling.nodeName != 'BUTTON') || document.activeElement.nodeName == 'BUTTON'){ 323 324 // Check if parent submenu column contains embed web-article links and focus those 325 if(parentNodeEl.nextElementSibling.classList.contains('mega-menu__link-group')){ 326 const linkGroupLinks = parentNodeEl.nextElementSibling.querySelectorAll('.journal-content-article a'); 327 328 if(linkGroupLinks.length > 0){ 329 linkGroupLinks[0].focus(); 330 e.preventDefault(); 331 } 332 else{ 333 // last item of parent columns is web-article-link-group, but contains
333no links 334 const topLevelParent = document.activeElement.closest('.mega-menu__top-level__item'); 335 336 // focus next main nav's top level link 337 if(topLevelParent){ 338 topLevelParent.nextElementSibling.children[0].focus(); 339 e.preventDefault(); 340 } 341 } 342 } 343 else{ 344 parentNodeEl.nextElementSibling.children[0].focus(); 345 e.preventDefault(); 346 } 347 } 348 } 349 } 350 // First column (#0) 351 else{ 352 const containsEmptyWebArticle = document.activeElement.parentNode.nextElementSibling?.classList.contains('mega-menu__link-group') ? document.activeElement.parentNode.nextElementSibling.children : 0; 353 const webArticleLinks = [...activeColumn[0]?.querySelectorAll('.mega-menu__link-group .journal-content-article a')]; // ember web-article links on column-0 354 const hasSubmenus = document.activeElement.nextElementSibling?.nodeName == 'BUTTON'; 355 const toplevelLI = document.activeElement.closest('.mega-menu__top-level__item '); 356 357 // Embed web-article 358 if(containsEmptyWebArticle.length == 0 && !hasSubmenus){ 359 const currentTopLevelItem = toplevelLI.nextElementSibling?.children[0]; 360 // Return to main nav level if activeElement is last item on column-0 361 if(currentTopLevelItem){ 362 currentTopLevelItem.focus(); 363 e.preventDefault(); 364 } 365 else{ 366 document.querySelector('#main-content a').focus(); 367 e.preventDefault(); 368 } 369 } 370 else if(webArticleLinks || hasBottomRowLinks){ 371 const bottomRowLinks = [...lastMegamenuList.querySelectorAll('a')]; 372 const isLastLinkGroupLink = document.activeElement == webArticleLinks[webArticleLinks.length - 1]; 373 374 if((isLastLinkGroupLink && !hasBottomRowLinks)){ 375 const currentTopLevelItem = toplevelLI.nextElementSibling.children[0]; 376 377 if(currentTopLevelItem){ 378 currentTopLevelItem.focus(); 379 e.preventDefault(); 380 } 381 } 382 else if(isLastLinkGroupLink && hasBottomRowLinks){ 383 bottomRowLinks[0].focus(); 384 e.preventDefault() 385 } 386 } 387 } 388 } 389 } 390} 391 392// Show Mobile menu 393function showMobileMegamenu() { 394 const mobileMegamenuToggler = document.getElementById('header__top__mobile-menu__button'); 395 const mobileMegamenuTogglericon = mobileMegamenuToggler.querySelector('.pirha-icon'); 396 const mobileMegamenu = document.getElementById("header__mega-menu__mobile"); 397 398 const documentBody = document.querySelector('body'); 399 mobileMegamenuToggler.getAttribute('aria-expanded') == 'true' ? documentBody.classList.remove('mobile-menu-open') : documentBody.classList.add('mobile-menu-open'); 400 401 if (mobileMegamenu !== null && mobileMegamenu.style.display !== "none") { 402 mobileMegamenuToggler.setAttribute('aria-expanded','false'); 403 mobileMegamenuTogglericon.classList.remove('pirha-icon--close'); 404 mobileMegamenuTogglericon.classList.add('pirha-icon--menu'); 405 406 hideMegamenu(); 407 } else { 408 mobileMegamenuToggler.setAttribute('aria-expanded','true'); 409 mobileMegamenuTogglericon.classList.remove('pirha-icon--menu'); 410 mobileMegamenuTogglericon.classList.add('pirha-icon--close'); 411 412 // showBackDrop("main-content"); 413 if (mobileMegamenu !== null) { 414 mobileMegamenu.style.display = "block"; 415 } 416 417 setMobileNavigationHeight(); 418 } 419 420 421 // Find the active panel item in special panels 422 const activeSpecialPanelItem = document.querySelector('.special-panel .panel-item.active'); 423 424 if (activeSpecialPanelItem) { 425 // Find which special panel contains this active item 426 const activeSpecialPanel = activeSpecialPanelItem.closest('.special-panel'); 427 428 if (activeSpecialPanel) { 429 const panelId = activeSpecialPanel.getAttribute('data-panel-id'); 430 // console.log('Active special panel ID:', panelId); 431 432 // Use existing showPanel function 433 showPanel(panelId, "250", true); 434 435 // // Set up focus management for this panel 436 // manageMobileMenuFocus({ target: activeSpecialPanel, targetCurrentPage: true }); 437 } 438 } 439} 440 441function hideMenu(menuToHide) { 442 const mobileMegamenu = document.getElementById("header__mega-menu__mobile"); 443 const canHideMegamenu = mobileMegamenu !== null && mobileMegamenu.style.display !== "none"; 444 445 if(canHideMegamenu){ 446 document.querySelector('body').classList.remove('mobile-menu-open'); 447 } 448 449 switch (menuToHide) { 450 case "all": 451 if (canHideMegamenu) hideMegamenu(); 452 if (document.querySelector("#header__mega-menu__bar .mega-menu__top-level__item__content[aria-expanded='true']")) { 453 hideDesktopMegamenu(); 454 } 455 const mobileMenuToggler = document.querySelector('#header__top__mobile-menu__button'); 456 updateMobileMenuToggler(mobileMenuToggler); 457 break; 458 case "mega": 459 if (canHideMegamenu) hideMegamenu(); 460 } 461} 462 463function hideMegamenu() { 464 const mobileMegamenu = document.getElementById("header__mega-menu__mobile"); 465 mobileMegamenu.style.display = "none"; 466} 467 468function hideDesktopMegamenu() {
469 const expandedTopLevelItem = document.querySelector("#header__mega-menu__bar .mega-menu__top-level__item__content[aria-expanded='true']"); 470 if(expandedTopLevelItem){ 471 expandedTopLevelItem.focus(); 472 expandedTopLevelItem.setAttribute('aria-expanded', 'false'); 473 } 474} 475 476document.querySelectorAll('.portlet').forEach(function (element) { 477 // Brings down the portlet-header element more down when the user does not have permissions to edit portlets 478 element.addEventListener("mouseover", function () { 479 const isPortletTopper = element.firstElementChild.classList.contains("portlet-topper"); 480 if (!isPortletTopper) { 481 const nodeList = element.childNodes; 482 for (let i = 0; i < nodeList.length; i++) { 483 for (let j = 0; j < nodeList[i].childNodes.length; j++) { 484 if (nodeList[i].childNodes[j].classList !== undefined && nodeList[i].childNodes[j].classList.contains("portlet-header")) { 485 const portletHeader = element.getElementsByClassName(nodeList[i].childNodes[j].classList)[0]; 486 portletHeader.setAttribute('style', 'top: -1.5rem !important'); 487 break; 488 } 489 } 490 } 491 } 492 }); 493}); 494 495 496/* PIRHA 2.0 */ 497/* ************** */ 498 499// Main navigation - Navigate top level links with arrow keys (left and right) 500function mainNavArrowKey([...topLevelItems]){ 501 const firstLink = topLevelItems[0]; 502 const lastLink = topLevelItems[topLevelItems.length - 1]; 503 504 topLevelItems.forEach(topLevelItem => { 505 topLevelItem.addEventListener('keydown', function(e) { 506 // Left 507 if (e.keyCode == 37) { 508 if(document.activeElement?.parentElement != firstLink){ 509 document.activeElement.parentElement.previousElementSibling?.children[0].focus(); 510 } 511 return false; 512 } 513 // Right 514 if (e.keyCode == 39) { 515 if(document.activeElement?.parentElement != lastLink){ 516 document.activeElement.parentElement.nextElementSibling?.children[0].focus(); 517 } 518 return false; 519 } 520 }) 521 }) 522} 523 524 525/* Toggle megamenu's submenus */ 526function toggleSubmenu(button){ 527 const targetSubmenuId = button.dataset.targetSubMenu; 528 const targetSubmenuLevel = Number(button.dataset.targetLevel) + 1; 529 const isExpanded = button.getAttribute('aria-expanded') == 'true' ? true : false; 530 let submenus; 531 532 // Select megamenu column(s) by targeted submenu level 533 if(targetSubmenuLevel == 1 && isExpanded){ 534 submenus = document.querySelectorAll( 535 `.mega-menu__dropdown .submenu-column-${targetSubmenuLevel} ul.open, 536 .mega-menu__dropdown .submenu-column-${targetSubmenuLevel+1} ul.open`); 537 } 538 else if(targetSubmenuLevel == 2){ 539 submenus = document.querySelectorAll( 540 `.mega-menu__dropdown .submenu-column-${targetSubmenuLevel} ul`); 541 } 542 else{ 543 submenus = document.querySelectorAll( 544 `.mega-menu__dropdown .submenus ul`); 545 } 546 submenus.forEach(submenu => submenu.classList.remove('open')); 547 548 // Get targeted submenu by id 549 const targetedSubmenu = [...submenus].filter( submenu => submenu.dataset.parentId == targetSubmenuId ); 550 551 if(!isExpanded){ 552 targetedSubmenu[0].classList.add('open') 553 } 554 555 controlAriaAttributes(button, submenus); 556 setDropdownHeight(button); 557} 558 559/* Set correct aria-attributes to megamenu items */ 560function controlAriaAttributes(element, submenus = ''){ 561 if(element !== null){ 562 const ariaExpanded = element.getAttribute('aria-expanded'); 563 564 // Reset submenus 565 if(submenus.length > 0){ 566 [...submenus].forEach(submenu => { 567 submenu.querySelector('button[aria-expanded="true"]')?.setAttribute('aria-expanded','false'); 568 569 const buttonSR = submenu.querySelector('button .sr-text') 570 if(buttonSR) buttonSR.textContent = translate('menu-open') 571 }) 572 } 573 574 // If expanded 575 if(ariaExpanded == 'true'){ 576 element.setAttribute('aria-expanded', 'false') 577 578 if(element.querySelector('.sr-text') != null){ 579 element.querySelector('.sr-text').textContent = translate('menu-open'); 580 } 581 } 582 else{ 583 // Reset Siblings 584 const allButtons = element.closest('ul').querySelectorAll('li button'); 585 586 allButtons.forEach(button => { 587 button.setAttribute('aria-expanded', 'false'); 588 589 if(button.querySelector('.sr-text') != null){ 590 button.querySelector('.sr-text').textContent = translate('menu-open'); 591 } 592 }); 593 594 // Set clicked 595 element.setAttribute('aria-expanded', 'true') 596 597 if(element.querySelector('.sr-text') != null){ 598 element.querySelector('.sr-text').textContent = translate('menu-close'); 599 } 600 } 601 } 602} 603 604/* Set height to megamenu's submenus */ 605function setDropdownHeight(clickedButton = null){ 606 if(clickedButton){ 607 let targetSubmenu = ''; 608 let headerHeight = '';
609 let mmContainerHeightCSS = 'height: auto;'; 610 let bottomRowHeight = 0; 611 612 // get css variable if it exists 613 const bottomRowCSSVarValue = getComputedStyle(document.documentElement) 614 .getPropertyValue('--mm-bottom-row-height') 615 .trim(); 616 617 if(bottomRowCSSVarValue){ 618 bottomRowHeight = parseFloat(bottomRowCSSVarValue); 619 } 620 621 const isButtonExpanded = clickedButton.getAttribute('aria-expanded') == 'true'; 622 const isTopLevelButton = clickedButton.classList.contains('mega-menu__top-level__item__content'); 623 const isTopLevelMinimenuButton = clickedButton.classList.contains('minimenu_button'); 624 const isSubmenuToggleButton = clickedButton.classList.contains('toggle-submenu'); 625 const containers = document.querySelectorAll('.mega-menu--container'); 626 627 if(isTopLevelButton){ 628 headerHeight = clickedButton.nextElementSibling.getBoundingClientRect().top; 629 targetSubmenu = clickedButton.nextElementSibling.querySelector('ul.level-0'); 630 menuColumnHeight = 0; 631 menuColumnsOpen = clickedButton.nextElementSibling.querySelectorAll('.mega-menu__dropdown__list.open'); 632 633 if(menuColumnsOpen){ 634 menuColumnsOpen.forEach(column => { 635 if(menuColumnHeight < column.offsetHeight){ 636 menuColumnHeight = column.offsetHeight; 637 targetSubmenu = column; 638 } 639 }) 640 } 641 642 if(!isTopLevelMinimenuButton){ 643 const currentContainer = clickedButton.nextElementSibling.querySelector('.mega-menu--container'); 644 currentSubmenu = currentContainer.querySelector('.mega-menu__dropdown__list'); 645 const buttomRow = currentContainer?.querySelector('.mega-menu__bottom-row'); 646 647 // set css variable if mega-menu bottowRow exists 648 if(buttomRow){ 649 bottomRowHeight = buttomRow.offsetHeight; 650 document.documentElement.style.setProperty('--mm-bottom-row-height', bottomRowHeight + 'px'); 651 } 652 } 653 } 654 else if(isSubmenuToggleButton){ 655 headerHeight = clickedButton.closest('.mega-menu__dropdown').getBoundingClientRect().top; 656 targetSubmenu = document.querySelector(`ul[data-parent-id="${clickedButton.dataset.targetSubMenu}"]`); 657 currentSubmenu = clickedButton.closest('.mega-menu__dropdown__list'); 658 } 659 660 let tallestSub = 0; 661 containers.forEach(cont => { 662 if(cont.offsetHeight > 0){ 663 cont.querySelectorAll('ul.open').forEach(submenu => { 664 if(submenu.offsetHeight > tallestSub){ 665 tallestSub = submenu.offsetHeight 666 } 667 }); 668 } 669 }); 670 671 // Set heights 672 if(!isTopLevelMinimenuButton){ 673 if(tallestSub >= (window.innerHeight - headerHeight)){ 674 mmContainerHeightCSS = 'height: '+ Math.round(tallestSub) + 'px;'; 675 } 676 else if( isButtonExpanded ){ 677 mmContainerHeightCSS = 'height: auto;'; 678 } 679 680 targetSubmenu.closest('.mega-menu--container').setAttribute('style', mmContainerHeightCSS +' max-height: calc(100vh - '+ Math.round(headerHeight) +'px);'); 681 } 682 } 683} 684 685 686/* Get parent LI elements */ 687function getParentLIs(el) { 688 var parents = []; 689 while (el) { 690 if (el.nodeName === 'LI') { 691 parents.push(el); 692 } 693 694 el = el.parentNode; 695 } 696 return parents; 697} 698 699function openActiveSubmenuParents(){ 700 const megamenu = document.querySelector('#header__mega-menu'); 701 702 if(megamenu){ 703 const currentPage = megamenu.querySelector('.mega-menu__dropdown__list__item[aria-current="page"]'); 704 const toggleBtns = megamenu.querySelectorAll('.toggle-submenu'); 705 const openSubmenus = megamenu.querySelectorAll('ul.open'); 706 let parentID = !!currentPage ? currentPage.closest('ul').dataset.parentId : null; 707 708 // Reset menus 709 if(currentPage && !currentPage.closest('ul').classList.contains('open')){ 710 toggleBtns.forEach(btn => { 711 btn.setAttribute('aria-expanded', 'false'); 712 }); 713
714 openSubmenus.forEach(submenu => { 715 submenu.classList.contains('level-0') ? '' : submenu.classList.remove('open'); 716 }); 717 718 currentPage.focus(); 719 currentPage.closest('.mega-menu__top-level__item ').setAttribute('aria-current', 'true'); 720 currentPage.closest('ul').classList.add('open'); 721 } 722 else{ 723 toggleBtns.forEach(btn => { 724 if(!btn.closest('li').hasAttribute('aria-current')){ 725 btn.setAttribute('aria-expanded', 'false'); 726 } 727 }); 728 729 openSubmenus.forEach(submenu => { 730 submenu.classList.contains('level-0') ? '' : submenu.classList.remove('open'); 731 }); 732 733 currentPage?.closest('ul').classList.add('open'); 734 } 735 736 if(parentID){ 737 let parentToggleMenuBtn = megamenu.querySelector('.toggle-submenu[data-target-sub-menu="'+ parentID +'"'); 738 let currentMenuLevel = currentPage.closest('ul').dataset.level; 739 740 /* Based on current page menu level, open parent menus, set aria-attributes and sr-texts */ 741 while (currentMenuLevel >= 0) { 742 if(!!parentToggleMenuBtn){ 743 parentToggleMenuBtn.setAttribute('aria-expanded', 'true'); 744 parentToggleMenuBtn.closest('li').setAttribute('aria-current', 'true'); 745 parentToggleMenuBtn.querySelector('.sr-text').textContent = translate('menu-close'); 746 parentToggleMenuBtn.closest('ul').classList.add('open'); 747 748 parentID = parentToggleMenuBtn.closest('ul').dataset.parentId; 749 parentToggleMenuBtn = megamenu.querySelector('.toggle-submenu[data-target-sub-menu="'+ parentID +'"'); 750 } 751 752 currentMenuLevel--; 753 } 754 } 755 } 756} 757 758/* External links */ 759/* -------------- */ 760function isValidUrl(url){ 761 try { 762 new URL(url); 763 } catch (e) { 764 return false; 765 } 766 767 return true; 768} 769 770function detectExternalLinks(item) { 771 const locationHostname = window.location.hostname; 772 const links = document.querySelectorAll(item); 773 774 for (let i = 0; i < links.length; i++) { 775 if( isValidUrl(links[i]) ){ 776 let domain = new URL(links[i]); 777 let linkHostname = domain.hostname.replace('www.', ''); 778 779 if (linkHostname && locationHostname.replace('www.', '') !== linkHostname) { 780 const hasButtonAsChild = links[i].querySelector('[class*="pirha-button"]') ? true : false; 781 const containsClass = /pirha-button/.test(links[i].className) 782 783 if(!hasButtonAsChild && !containsClass){ 784 /* Add external link css class */ 785 addExternalLinkStyling(links[i]); 786 } 787 } 788 } 789 } 790} 791 792/* Add external link css class */ 793function addExternalLinkStyling(link) { 794 const UNDESIRABLE_PARENTS = ['.portlet-topper', '.pagination', '.no-external']; 795 let addIcon = true; 796 797 for (let i = 0; i < UNDESIRABLE_PARENTS.length; i++) { 798 if(link.closest(UNDESIRABLE_PARENTS[i]) != null){ 799 addIcon = false; 800 break; 801 } 802 } 803 804 if(addIcon){ 805 link.classList.add('external-link'); 806 } 807} 808 809 810/* Pirha tabs with active tab indicator */ 811/* ------------------------------------ */ 812function controlTabs(target) { 813 const tabs = document.querySelectorAll(target + ' [role="tab"]'); 814 let activeTab = document.querySelector(target + ' .active [role="tab"]'); 815 const tabIndicator = document.querySelector(target + ' .tab-indicator'); 816 817 if(tabs.length > 0){ 818 /* Check tab visibility and set active tab indicator to correct position */ 819 if(!!activeTab){ 820 activeTab = tabs[0]; 821 822 onVisible(activeTab, () => { 823 setTabIndicator(activeTab, tabIndicator); 824 }); 825 } 826
827 tabs.forEach((tab) => { 828 tab.addEventListener('click', (e) => { 829 const tabAriaSelected = tab.getAttribute('aria-selected'); 830 setTabIndicator(tab, tabIndicator); 831 832 /* Reset filters when changing active tab */ 833 document.querySelectorAll(target + ' .tab-content .btn-check').forEach(checkbox => checkbox.checked = false); 834 835 if(tabAriaSelected != 'true'){ 836 // Deactivate other tabs 837 tabs.forEach(item => { 838 item.setAttribute('aria-selected', 'false'); 839 item.parentElement.classList.remove('active'); 840 }) 841 842 // Activate clicked tab 843 tab.setAttribute('aria-selected', 'true'); 844 tab.parentElement.classList.add('active'); 845 846 // Hide other tab panels 847 document.querySelectorAll(target + ' [role="tabpanel"]').forEach(item => { 848 item.classList.remove('show', 'active') 849 }) 850 851 // Show clicked tab panel 852 const targetPanelEl = document.querySelector('#' + tab.getAttribute('aria-controls')) 853 854 if(targetPanelEl){ 855 targetPanelEl.classList.add('show', 'active'); 856 } 857 } 858 }); 859 }) 860 } 861} 862 863function setTabIndicator(activeTab, tabIndicator) { 864 if(!!tabIndicator && !!activeTab){ 865 tabIndicator.style.left = activeTab.offsetLeft + 'px'; 866 tabIndicator.style.width = activeTab.offsetWidth + 'px'; 867 } 868} 869 870// Check when element is visible (not display: none) 871function onVisible(element, callback) { 872 if(element){ 873 new IntersectionObserver((entries, observer) => { 874 entries.forEach(entry => { 875 if(entry.intersectionRatio > 0) { 876 callback(element); 877 observer.disconnect(); 878 } 879 }); 880 }).observe(element); 881 } 882} 883 884 885/* Pirha mobile navigation */ 886/* ------------------------------------ */ 887function setMobileNavigationHeight(){ 888 const pageHeader = document.querySelector('#header'); 889 const mnavStyleTag = document.querySelector('#mnavHeight'); 890 891 if(mnavStyleTag){ 892 mnavStyleTag.remove(); 893 } 894 895 const visibleHeight = window.innerHeight; 896 const hasMinisiteClass = /cs-/.test(document.body.className); 897 let headerComponentsHeight = 0; 898 899 if(hasMinisiteClass){ 900 const headEls = pageHeader.querySelectorAll('.header__content > div'); 901 902 headEls.forEach(el => { 903 headerComponentsHeight += el.offsetHeight 904 }) 905 } 906 else{ 907 headerComponentsHeight = pageHeader.querySelector('.header__content .header__top').offsetHeight 908 } 909 910 const css = 911 `#header__mega-menu__mobile .pirha-mobilenav-container, 912 #header__mega-menu__mobile .menu-list--wrapper, 913 #header__mega-menu__mobile .menu-list--wrapper.current{ 914 height: calc(${visibleHeight}px - ${headerComponentsHeight}px - ${pageHeader.offsetTop}px); 915 }`; 916 917 let styleTag = document.createElement('style'); 918 styleTag.id = 'mnavHeight'; 919 styleTag.appendChild(document.createTextNode(css)); 920 document.body.appendChild(styleTag); 921} 922 923// // Show panel for special menus 924function showPanel(panelId, timeout = "250", skipAnimation = false) { 925 const targetPanel = document.querySelector('.panel[data-panel-id="' + panelId + '"]'); 926 927 if (!targetPanel) { 928 //console.warn('Panel not found:', panelId); 929 return; 930 } 931 932 if (skipAnimation) { 933 targetPanel.classList.add('active'); 934 manageMobileMenuFocus({ target: targetPanel, targetCurrentPage: true }); 935 936 } else { 937 targetPanel.classList.add('slide-in'); 938 939 setTimeout(() => { 940 targetPanel.classList.remove('slide-in'); 941 manageMobileMenuFocus({ target: targetPanel }); 942 }, timeout); 943 944 targetPanel.classList.add('active'); 945 } 946} 947 948function mobileNavigation(){ 949 const mobileNavigation = document.querySelector('.pirha-mobile-navigation'); 950 951 if(mobileNavigation){ 952 const openSubmenuButtons = mobileNavigation.querySelectorAll('.mobile-toggle-submenu'); 953 const returnButtons = mobileNavigation.querySelectorAll('.return-button'); 954 const currentPage = mobileNavigation.querySelector('[aria-current="page"]'); 955 const timeout = "300"; 956 957 const specialMenuTriggers = mobileNavigation.querySelectorAll('.special-panel .panel-trigger'); 958 const specialMenuReturnButtons = mobileNavigation.querySelectorAll('.special-panel .back-button'); 959 960 961 if(!!currentPage){ 962 const currentMenuSection = currentPage.closest('.menu-list--wrapper'); 963 currentPage.classList.add('submenu-visible'); 964 965 /* Clear current class */ 966 mobileNavigation.querySelector('.current')?.classList.remove('current'); 967 968 /* Open submenu of current page if child items exists */ 969 !!currentPage.querySelector('.menu-list--wrapper') 970 ? currentPage.querySelector('.menu-list--wrapper').classList.add('current') 971 : currentMenuSection?.classList.add('current'); 972 973 if(currentMenuSection){ 974 let parentSectionLevel = currentMenuSection.dataset.level - 1; 975 let parentLI = currentMenuSection.closest('li.menu-item'); 976 let parentWrapper = parentLI?.closest('.menu-list--wrapper') 977 978 /* Make nested submenu tree visible relative to current page */ 979 while (parentSectionLevel > 0) { 980 parentSectionLevel = parentSectionLevel - 1; 981 982 parentLI.classList.add('submenu-visible'); 983 parentLI.setAttribute('aria-current', 'true'); 984 parentLI.querySelector('.mobile-toggle-submenu').setAttribute('aria-expanded', 'true'); 985 parentLI = parentWrapper.closest('li.menu-item'); 986 parentWrapper = parentWrapper.closest('[data-level="'+ parentSectionLevel +'"]'); 987 } 988 } 989 } 990 991 /* Slide out SPECIAL submenus */ 992 specialMenuReturnButtons.forEach(button => { 993 button.addEventListener('click', () => { 994 const panelId = button.closest('.special-panel').dataset.panelId 995 const parentPanelId = button.closest('.special-panel').dataset.parentPanel 996 const parentPanel = document.querySelector('[data-panel-id="'+ parentPanelId +'"]'); 997 998 button.closest('.special-panel').classList.add('slide-out'); 999 1000 if(parentPanel){ 1001 parentPanel.classList.add('active'); 1002 1003 setTimeout(() => { 1004 const parentBtn = parentPanel.querySelector('button[data-target="' + panelId + '"]') 1005 parentBtn.setAttribute('aria-expanded', 'false'); 1006 1007 button.closest('.special-panel').classList.remove('active', 'slide-out'); 1008 }, timeout); 1009 } 1010 1011 setLastNavigationTrigger(button); 1012 manageMobileMenuFocus({ target: parentPanel, isReturningToParent: true }); 1013 }) 1014 }) 1015 1016 // slide in special
1017 specialMenuTriggers.forEach(button => { 1018 button.addEventListener('click', (e) => { 1019 const targetPanelId = button.dataset.target 1020 button.setAttribute('aria-expanded', 'true'); 1021 1022 const parentPanelId = button.closest('.special-panel').dataset.parentPanel 1023 const parentPanel = document.querySelector('[data-panel-id="'+ parentPanelId +'"]'); 1024 parentPanel?.classList.add('active'); 1025 1026 setLastNavigationTrigger(button); 1027 showPanel(targetPanelId, timeout) 1028 }) 1029 }) 1030 1031 /* Slide out regular submenus */ 1032 returnButtons.forEach(button => { 1033 button.addEventListener('click', () => { 1034 if(button.dataset.levelTarget){ 1035 button.closest('.menu-list--wrapper').classList.add('slide-out'); 1036 button.closest('[data-level="'+ button.dataset.levelTarget +'"]').classList.add('current'); 1037 1038 setTimeout(() => { 1039 button.closest('li').classList.remove('submenu-visible'); 1040 button.closest('li').querySelector('.mobile-toggle-submenu').setAttribute('aria-expanded', 'false'); 1041 button.closest('.menu-list--wrapper').classList.remove('current', 'slide-out'); 1042 }, timeout); 1043 1044 const parentWrapper = document.querySelector('.menu-list--wrapper.current[data-level="'+ button.dataset.levelTarget +'"]'); 1045 1046 setLastNavigationTrigger(button); 1047 manageMobileMenuFocus({ target: parentWrapper, isReturningToParent: true }); 1048 } 1049 }) 1050 }) 1051 1052 /* Slide in regular submenus */ 1053 openSubmenuButtons.forEach(button => { 1054 button.addEventListener('click', () => { 1055 button.nextElementSibling.classList.add('slide-in'); 1056 button.setAttribute('aria-expanded', 'true'); 1057 1058 setTimeout(() => { 1059 button.closest('.menu-list--wrapper')?.classList.remove('current'); 1060 button.closest('li').classList.add('submenu-visible'); 1061 button.nextElementSibling.classList.remove('slide-in'); 1062 1063 setLastNavigationTrigger(button); 1064 manageMobileMenuFocus(button.nextElementSibling); 1065 }, timeout); 1066 1067 button.closest('li').classList.add('submenu-visible'); 1068 button.nextElementSibling.classList.add('current'); 1069 }) 1070 }) 1071 } 1072} 1073 1074 1075// Helper to store the navigation trigger 1076let lastNavigationTrigger = null; 1077function setLastNavigationTrigger(element) { 1078 lastNavigationTrigger = element; 1079} 1080 1081// Manage focus 1082function manageMobileMenuFocus({ target = null, targetCurrentPage = false, isReturningToParent = false } = {}) { 1083 // Find the current active panel 1084 const currentSection = target || 1085 document.querySelector('.pirha-mobilenav-container .menu-list--wrapper.current') || 1086 document.querySelector('.pirha-mobilenav-container .special-panel.active'); 1087 1088 if (!currentSection) { 1089 return; 1090 } 1091 1092 let elementToFocus = null; 1093 1094 // If returning to parent, focus the element that opened the child panel 1095 if (isReturningToParent && lastNavigationTrigger) { 1096 // main nav specific 1097 if (lastNavigationTrigger.classList.contains('return-button')) { 1098 const parentLi = lastNavigationTrigger.closest('li'); 1099 elementToFocus = parentLi?.querySelector('.mobile-toggle-submenu'); 1100 } 1101 1102 // Special nav specific 1103 else if (lastNavigationTrigger.classList.contains('back-button')) { 1104 const currentUl = lastNavigationTrigger.closest('ul'); 1105 const panelId = currentUl?.getAttribute('data-panel-id'); 1106 const parentPanelId = currentUl?.getAttribute('data-parent-panel'); 1107 1108 if (panelId && parentPanelId) { 1109 if (parentPanelId === 'root') { 1110 // returning to root 1111 const rootPanel = document.querySelector('.menu-list--wrapper.current'); 1112 elementToFocus = rootPanel?.querySelector(':scope > ul > li > .panel-trigger'); 1113 } else { 1114 // Find button with data-target matching the panel ID 1115 const specialPanel = document.querySelector('.special-panel[data-panel-id="' + parentPanelId + '"]') 1116 || document.querySelector('div[data-panel-id="' + parentPanelId + '"] > ul > .special-nav-entry'); 1117 elementToFocus = specialPanel?.querySelector(`button[data-target="${panelId}"]`); 1118 } 1119 } 1120 } 1121 1122 // Fallback: if we couldn't find the specific element, try to use the trigger itself 1123 if (!elementToFocus && currentSection.contains(lastNavigationTrigger)) { 1124 elementToFocus = lastNavigationTrigger; 1125 } 1126 1127 // Clear after use 1128 lastNavigationTrigger = null; 1129 } 1130 1131 // If targetCurrentPage is true, focus the current page item 1132 if (targetCurrentPage) { 1133 elementToFocus = currentSection.querySelector('.menu-item[aria-current="page"] .item') || 1134 currentSection.querySelector('.panel-item.active a'); 1135 } 1136 1137 // Focus the parent page item (item with aria-current="true") 1138 if (!elementToFocus) { 1139 elementToFocus = currentSection.querySelector('.menu-item[aria-current="true"] .item'); 1140 1141 // For special panels, check if there's a parent panel reference 1142 if (!elementToFocus && currentSection.classList.contains('special-panel')) { 1143 const parentPanelId = currentSection.getAttribute('data-parent-panel'); 1144 if (parentPanelId) { 1145 // Find the item in the parent panel that links to this panel 1146 const parentPanel = document.querySelector(`.mobile-navigation [data-panel-id="${parentPanelId}"]`); 1147 elementToFocus = parentPanel?.querySelector('.panel-item.active a'); 1148 } 1149 } 1150 } 1151 1152 // Focus the first focusable item in the panel 1153 if (!elementToFocus) { 1154 elementToFocus = currentSection.querySelector('.menu-item .item') || 1155 currentSection.querySelector('.panel-item a') || 1156 currentSection.querySelector('.menu-item--return button') || 1157 currentSection.querySelector('a[href], button'); 1158 } 1159 1160 // Focus the element if found 1161 if (elementToFocus) { 1162 elementToFocus.focus(); 1163 } 1164 1165 // Set up focus trap for this panel 1166 mobilenavTrapFocus(currentSection); 1167} 1168 1169/* Focus trap */ 1170// Store the current trap handler so we can remove it later 1171let currentTrapHandler = null; 1172 1173function mobilenavTrapFocus(element) { 1174 const mobilenavContainer = document.querySelector('.pirha-mobilenav-container'); 1175 1176 if (!mobilenavContainer) { return; } 1177 if (!element) { return; } 1178 1179 const eServiceButton = document.querySelector('.pirha-mobilenav-container .button-e-service'); 1180 const mobileClose = document.querySelector('.pirha-mobilenav-container .mobile-nav-close'); 1181 1182 if ((element.classList.contains('menu-list--wrapper') && element.classList.contains('current')) || 1183 (element.classList.contains('special-panel') && element.classList.contains('active'))) { 1184 wrapper = element; 1185 } else { 1186 wrapper = element.closest('.menu-list--wrapper.current, .special-panel.active'); 1187 } 1188 1189 if (!wrapper) { 1190 wrapper = document.querySelector('.menu-list--wrapper.current, .special-panel.active'); 1191 } 1192 1193 let focusableEls; 1194 1195 if (wrapper?.classList.contains('menu-list--wrapper')) { 1196 // For nested structure: get specific elements without traversing into nested panels 1197 const selectors = [ 1198 // Return button 1199 ':scope > .menu-item--return button', 1200 ':scope > .menu-item--return a[href]', 1201 1202 // Main menu items, direct children only 1203 ':scope > ul > li > a[href]', 1204 ':scope > ul > li > button', 1205 1206 // Topbar links 1207 ':scope > .topbar-links--mobile a[href]', 1208 ':scope > .topbar-links--mobile button', 1209 1210 // Language selection 1211 ':scope > .language-selection--mobile a[href]', 1212 ':scope > .language-selection--mobile button', 1213 1214 // Mega menu bottom row 1215 ':scope > .mega-menu__bottom-row a[href]', 1216 ':scope > .mega-menu__bottom-row button' 1217 ]; 1218 1219 focusableEls = wrapper.querySelectorAll(selectors.join(', ')); 1220 1221 // If this is the root panel, filter out items from .special-panel elements 1222 if (wrapper.dataset.panelId === 'root') { 1223 focusableEls = Array.from(focusableEls).filter(el => { 1224 return !el.closest('ul.special-panel'); 1225 }); 1226 } 1227 1228 } else if (wrapper?.classList.contains('special-panel')) { 1229 focusableEls = wrapper.querySelectorAll('a[href], button'); 1230 } 1231 1232 let filteredElements = [...(focusableEls || [])]; 1233 1234 // Add out of current panel/section links 1235 if(mobileClose !== null && eServiceButton !== null){ 1236 filteredElements = [mobileClose, eServiceButton, ...filteredElements]; 1237 } 1238 1239 if (filteredElements.length === 0) { 1240 return; 1241 } 1242 1243 // For screen reader users on touch screen devices 1244 // tabindex to all other than visible items 1245 // reduces the possibility to focus on "hidden" elements outside active menu section 1246 const allFocusableNavItems = mobilenavContainer.querySelectorAll('a[href]:not([disabled]), button:not([disabled])');
1247 allFocusableNavItems.forEach(element => { 1248 element.removeAttribute('tabindex'); 1249 }); 1250 1251 wrapper.setAttribute('tabindex', '-1'); 1252 1253 const elementsToHide = [...allFocusableNavItems].filter(item => !filteredElements.includes(item)); 1254 elementsToHide.forEach(element => { 1255 element.setAttribute('tabindex', '-1'); 1256 }); 1257 1258 // Keyboard navigation with Tab and Tab+shift keys 1259 var firstFocusableEl = filteredElements[0]; 1260 var lastFocusableEl = filteredElements[filteredElements.length - 1]; 1261 var KEYCODE_TAB = 9; 1262 1263 // Remove previous event listener if it exists 1264 if (currentTrapHandler) { 1265 mobilenavContainer.removeEventListener('keydown', currentTrapHandler); 1266 } 1267 1268 // Create new handler 1269 currentTrapHandler = function(e) { 1270 var isTabPressed = (e.key === 'Tab' || e.keyCode === KEYCODE_TAB); 1271 1272 if (!isTabPressed) { 1273 return; 1274 } 1275 1276 /* shift + tab */ 1277 if(e.shiftKey) { 1278 if (document.activeElement === firstFocusableEl) {
1279 lastFocusableEl.focus(); 1280 e.preventDefault(); 1281 } 1282 } 1283 /* tab */ 1284 else { 1285 if (document.activeElement === lastFocusableEl) { 1286 firstFocusableEl.focus(); 1287 e.preventDefault(); 1288 } 1289 } 1290 }; 1291 1292 // Add new event listener 1293 mobilenavContainer.addEventListener('keydown', currentTrapHandler); 1294} 1295 1296 1297// Pirha accordions 1298function initPirhaAccordions() { 1299 const accordions = document.querySelectorAll('.pirha-accordions'); 1300 if (accordions) { 1301 accordions.forEach(accordion => { 1302 accordionActions(accordion); 1303 accordionPaginator(accordion); 1304 accordionToggleAll(accordion); 1305 }) 1306 1307 openAccordionBasedOnUrlHash(); 1308 } 1309} 1310 1311function accordionActions(accordion) { 1312 const buttons = accordion.querySelectorAll('.pirha-accordion__toggler'); 1313 1314 if (buttons) { 1315 buttons.forEach(button => { 1316 const content = button.parentElement.nextElementSibling; 1317 const panel = button.closest('.pirha-accordion__item'); 1318 1319 button.addEventListener('click', () => { 1320 button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') === 'true' ? 'false' : 'true'); 1321 1322 if (content && panel) { 1323 if ( button.getAttribute('aria-expanded') === 'true') { 1324 panel.classList.add('pirha-accordion__item--open'); 1325 content.classList.remove('d-none'); 1326 } else { 1327 panel.classList.remove('pirha-accordion__item--open'); 1328 content.classList.add('d-none'); 1329 } 1330 } 1331 1332 updateUrlHash(button); 1333 }); 1334 }); 1335 } 1336} 1337 1338function updateUrlHash(clickedElement) { 1339 const accordionId = clickedElement.querySelector('span[id]')?.id; 1340 1341 if (accordionId && clickedElement.getAttribute('aria-expanded') === 'true') { 1342 history.replaceState(null, null, `#${accordionId}`); 1343 } 1344} 1345 1346function openAccordionBasedOnUrlHash() { 1347 const hash = window.location.hash?.split('?')[0]; 1348 1349 if (hash) { 1350 const targetElement = document.querySelector(hash); 1351 1352 if (targetElement) { 1353 const accordionButton = targetElement.closest('.pirha-accordion__toggler'); 1354 1355 if (accordionButton && accordionButton.getAttribute('aria-expanded') === 'false') { 1356 accordionButton.click(); 1357 } 1358 1359 setTimeout(() => { 1360 targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); 1361 }, 150); 1362 } 1363 } 1364} 1365 1366 1367function accordionPaginator(accordion) { 1368 const button = accordion.querySelector('.pirha-accordions__button--paginator'); 1369 const panels = [...accordion.querySelectorAll('.pirha-accordion__item')] 1370 1371 if (button && panels.length > 0) { 1372 const icon = button.querySelector('.pirha-icon'); 1373 const button_content = button.querySelector('.pirha-button__content') 1374 const panels_total = parseFloat(button.dataset.panels); 1375 const panels_visible = parseFloat(button.dataset.panelsShow); 1376 let allVisible = false; 1377 1378 if (panels_total && panels_visible) { 1379 button.addEventListener('click', () => { 1380 const firstHidden = panels.findIndex(item => item.classList.contains('d-none')); 1381 const lastVisible = firstHidden + panels_visible; 1382 const focusTarget = allVisible ? lastVisible : firstHidden; 1383 1384 if (!allVisible) { 1385 // Change visibility of panels 1386 for (let i = 0; i < panels.length; i++) { 1387 if (i < lastVisible) panels[i].classList.remove('d-none'); 1388 } 1389 } else { 1390 // Reset to start state 1391 for (let i = 0; i < panels.length; i++) { 1392 if (i > lastVisible) panels[i].classList.add('d-none'); 1393 } 1394 } 1395 1396 // Hide button if there is no more panels hidden 1397 if (!panels.find(item => item.classList.contains('d-none'))) { 1398 button_content.innerHTML = translate('accordion-paginator-show-less'); 1399 icon.classList.remove('pirha-icon--chevron-down'); 1400 icon.classList.add('pirha-icon--chevron-up'); 1401 1402 allVisible = true; 1403 } else { 1404 button_content.innerHTML = translate('accordion-paginator-show-more'); 1405 // Change icon back to chevron down 1406 if (icon.classList.contains('pirha-icon--chevron-up')) { 1407 icon.classList.remove('pirha-icon--chevron-up'); 1408 icon.classList.add('pirha-icon--chevron-down'); 1409 } 1410 1411 allVisible = false;
1412 } 1413 1414 // Move focus to the first panel that was set visible 1415 setTimeout(function(){ 1416 const focusable = panels[focusTarget].querySelector('.pirha-accordion__toggler'); 1417 if (focusable) focusable.focus(); 1418 },200); 1419 }); 1420 } 1421 } 1422}; 1423 1424function accordionToggleAll(accordion) { 1425 const button = accordion.querySelector('.pirha-accordions__toggle--all'); 1426 const panels = [...accordion.querySelectorAll('.pirha-accordion__item')]; 1427 1428 if (button && panels.length > 0) { 1429 let isOpen = false; 1430 button.addEventListener('click', () => { 1431 panels.forEach(panel => { 1432 1433 const toggler = panel.querySelector('.pirha-accordion__toggler'); 1434 const content = panel.querySelector('.pirha-accordion__body'); 1435 1436 if (isOpen) { 1437 toggler.setAttribute('aria-expanded','false'); 1438 content.classList.add('d-none'); 1439 panel.classList.remove('pirha-accordion__item--open'); 1440 } else { 1441 toggler.setAttribute('aria-expanded','true'); 1442 content.classList.remove('d-none'); 1443 panel.classList.add('pirha-accordion__item--open') 1444 } 1445 }); 1446 1447 if (!isOpen) { 1448 accordion.classList.add('pirha-accordions__open--all'); 1449 button.innerHTML = translate('accordion-close-all'); 1450 isOpen = true; 1451 } else { 1452 accordion.classList.remove('pirha-accordions__open--all'); 1453 button.innerHTML = translate('accordion-open-all'); 1454 isOpen = false; 1455 } 1456 1457 }); 1458 } 1459} 1460 1461function translate(word) { 1462 if (word === "") return; 1463 let lang = "fi"; 1464 const i18n = { 1465 "fi": { 1466 "accordion-open-all": "Avaa kaikki", 1467 "accordion-close-all": "Sulje kaikki", 1468 "accordion-paginator-show-more": "Näytä lisää", 1469 "accordion-paginator-show-less": "Näytä vähemmän", 1470 "menu-open": "Avaa valikko", 1471 "menu-close": "Sulje valikko" 1472 }, 1473 "en": { 1474 "accordion-open-all": "Show all", 1475 "accordion-close-all": "Close all", 1476 "accordion-paginator-show-more": "Show more", 1477 "accordion-paginator-show-less": "Show less", 1478 "menu-open": "Open menu", 1479 "menu-close": "Close menu" 1480 } 1481 } 1482 const langFull = Liferay.ThemeDisplay.getLanguageId(); 1483 1484 if (langFull.split("_")[0] !== 'fi') { 1485 lang = 'en'; //langFull.split("-")[0]; 1486 } 1487 1488 return i18n[lang][word]; 1489} 1490 1491 1492// Pirha 2.0 Create Anchor links 1493// for structure "Ankkurilinkit" 1494// ------------------------------------------------------------------------------------------- 1495 1496function getAnchorLists() { 1497 const anchorLists = [...document.querySelectorAll('.pirha-anchorlist')]; 1498 1499 if (anchorLists.length > 0) { 1500 anchorLists.forEach(list => initAnchorLists(list)); 1501 } 1502} 1503 1504function initAnchorLists(list) { 1505 const layoutColumn = list.closest('#main-content').querySelector("#column-3"); 1506 const anchorlistContainer = list.querySelector('.pirha-anchorlist__content'); 1507 1508 if (anchorlistContainer) { 1509 const selectedDatatypesArr = anchorlistContainer.dataset.content.split(','); 1510 1511 if (selectedDatatypesArr.length > 0) { 1512 const targetHeadings = 'h1, h2:not(.portlet-title-text), h3' 1513 const targetElements = layoutColumn.querySelectorAll(targetHeadings) 1514 const filteredData = filterElementsByType(targetElements, selectedDatatypesArr) 1515 1516 createAnchors(filteredData, anchorlistContainer) 1517 } 1518 } 1519} 1520 1521function createAnchors(elements, container) { 1522 if(elements){ 1523 // Create a map to track anchor IDs 1524 const urlCountMap = new Map(); 1525 1526 // Process each object in the array 1527 const anchorData = elements.map(element => { 1528 const anchorText = element.innerText.trim(); 1529 const anchorId = anchorText.toLowerCase().replace(/ä|å/g,"a").replace(/ö/g,"o").replace(/[^\w\s]/gi, "").replace(/ /g,"-"); 1530 const count = urlCountMap.get(anchorId) || 0; 1531 1532 urlCountMap.set(anchorId, count + 1); 1533
1534 // Rename the anchor id if it's a duplicate 1535 const uniqueId = count > 0 ? `${anchorId}-${count}` : anchorId; 1536 1537 element.setAttribute('id', uniqueId) 1538 1539 return { url: uniqueId, text: anchorText }; 1540 }); 1541 1542 if (container.dataset.manual == 'false'){ 1543 createAnchorlinkList(anchorData, container) 1544 } 1545 } 1546} 1547 1548function createAnchorlinkList(data, container){ 1549 if(data && container){ 1550 const ul = container.querySelector('ul') 1551 1552 // Change order if selected Alphabetical listing 1553 if (container.dataset.order !== "undefined" && container.dataset.order == 'alphabetical') data.sort((a,b) => a.text.localeCompare(b.text)); 1554 1555 // Create link 1556 data.forEach(link => { 1557 let li = document.createElement('li'); 1558 let a = document.createElement('a'); 1559 a.href = "#" + link.url; 1560 a.innerText = link.text; 1561 li.appendChild(a); 1562 ul.appendChild(li); 1563 }); 1564 } 1565} 1566 1567function filterElementsByType(targetElements, selectedDatatypesArr){ 1568 const unDesiredParents = ['.pirha-anchorlist', '.pirha-accordion__body', '.d-none', '.cta-lifts'] 1569 // exception is a child element of ".journal-content-article" that would otherwise be filtered out when type: "webcontent" is unselected 1570 const childClassExceptions = ['.pirha-accordions'] 1571 1572 let filtered = Array.from(targetElements); 1573 filtered = filtered.filter(element => !element.closest(unDesiredParents)) 1574 1575 let parentCSSselectors = []; 1576 selectedDatatypesArr.forEach(type => { 1577 switch (type) { 1578 case "webcontent": 1579 parentCSSselectors.push(".journal-content-article"); 1580 break; 1581 case "accordion": 1582 parentCSSselectors.push(".pirha-accordions"); 1583 break; 1584 case "ptv": 1585 parentCSSselectors.push("div[class*='_ptv_widget']"); 1586 } 1587 }) 1588 1589 if(parentCSSselectors.length > 0){ 1590 // Filter out elements with unselected types and include exceptions if an exception is a child element of a parent selector 1591 return filtered.filter(element => { 1592 // Allow accordion + journal-article combination if journal-article is not selected 1593 const childException = !element.closest(parentCSSselectors.join())?.classList.contains(childClassExceptions.join()) 1594 && element.closest(parentCSSselectors.join())?.querySelector(childClassExceptions.join()) ? true : false; 1595 1596 const isJournalArticleH1 = element.nodeName === 'H1' 1597 && element.closest('.journal-content-article') 1598 && !element.closest('.journal-content-article').querySelector(childClassExceptions.join()) ? true : false; 1599 1600 if(isJournalArticleH1){ 1601 return !element 1602 } 1603 1604 if(childException){ 1605 return !element.closest(parentCSSselectors.join()) 1606 } 1607 else{ 1608 return element.closest(parentCSSselectors.join()) 1609 } 1610 }); 1611 } 1612} 1613 1614 1615/* 1616 Alloy image viewer from Intranet 1617 */ 1618function addAUimageViewers() { 1619 const myGalleryDivs = document.querySelectorAll('div.intra-aui-image-viewer-container, div.intra-aui-image-viewer-gallery'); 1620 const firstChildDivs = []; 1621 myGalleryDivs.forEach(div => { 1622 const firstChildDiv = div.querySelector('div'); 1623 if (firstChildDiv) { 1624 firstChildDivs.push(firstChildDiv); 1625 } 1626 }); 1627 1628 firstChildDivs.forEach(div => { 1629 let captionP = div.querySelector(`.intra-alloy-caption`); 1630 let caption = ""; 1631 if (captionP) { 1632 caption = captionP.textContent.trim(); 1633 if (caption === "") { 1634 captionP.remove(); 1635 } 1636 } 1637 let showThumbnails = div.querySelectorAll('a').length > 1; 1638 YUI().use('aui-image-viewer', function (Y) { 1639 let imageViewer = new Y.ImageViewer({ 1640 caption: caption, 1641 centered: true, 1642 imageAnim: { 1643 duration: 0.5, 1644 easing: 'easeIn' 1645 }, 1646 links: '#' + div.id + ' a', 1647 showInfo: false, 1648 showPlayer: false, 1649 zIndex: 999, 1650 infoTemplate: '', 1651 thumbnailsConfig: showThumbnails, 1652 }).render(); 1653 let backdrop = document.querySelector('.yui3-widget-bd'); 1654 let imageElement = document.querySelector('.image-viewer-base-image'); 1655 function stopPropagationHandler(event) { 1656 event.stopPropagation(); 1657 } 1658 1659 imageElement.addEventListener('click', stopPropagationHandler); 1660 backdrop.addEventListener('click', function () { 1661 imageViewer.hide(); 1662 }); 1663 }) 1664 }) 1665 1666} 1667 1668/* Begin lifts to equal height script */ 1669const changeToEqualHeight = () => { 1670 let layoutRows = Array.from(document.querySelectorAll('.portlet-layout.row')); 1671 1672 layoutRows.forEach((row, index) => { 1673 const nestedPortlets = row.querySelectorAll('.portlet-nested-portlets'); 1674 const hasPirhaSidebar = row.querySelector('.pirha-sidebar') ? true : false;
1675 1676 /* if row contains nested-portlets */ 1677 if(nestedPortlets.length > 0){ 1678 nestedPortlets.forEach(nestedPortlet => { 1679 const nestedLayoutRows = nestedPortlet.querySelectorAll('.portlet-layout'); 1680 1681 nestedLayoutRows.forEach(nlRow => { 1682 const nestedLifts = nlRow.querySelectorAll('.pirha-lift'); 1683 liftsToEqualHeight(nestedLifts); 1684 }); 1685 }) 1686 } 1687 1688 /* Lift rows outside nested-portlets */ 1689 else{ 1690 let lifts = row.querySelectorAll('.pirha-aihepiiri .pirha-lift'); 1691 1692 if (lifts.length > 0) { 1693 if(hasPirhaSidebar){ 1694 lifts = [...lifts].filter(lift => lift.closest('.portlet-nested-portlets')) 1695 } 1696 liftsToEqualHeight(lifts); 1697 } 1698 } 1699 }) 1700} 1701 1702//Changing height is not needed if the row has only one lift. 1703const liftsToEqualHeight = (lifts) => { 1704 let maxHeight = 0; 1705 const mqlTablet = window.matchMedia("(max-width: 767px)"); 1706 1707 if(!mqlTablet.matches){ 1708 lifts.forEach(lift => { 1709 lift.style.minHeight = 'auto'; 1710 const liftHeight = lift.getBoundingClientRect().height 1711 maxHeight = Math.max(maxHeight, liftHeight); 1712 }) 1713 } 1714 1715 lifts.forEach(lift => { 1716 lift.style.minHeight = maxHeight + 'px'; 1717 }) 1718} 1719 1720//If there's lifts then keep them same height. 1721if(document.querySelectorAll('.pirha-lift').length > 1){ 1722 let timeId = null; 1723 1724 const resize = () => { 1725 clearTimeout(timeId); 1726 timeId = setTimeout(changeToEqualHeight, 100); 1727 } 1728 1729 window.addEventListener('load', resize) 1730 window.addEventListener("resize", resize) 1731} 1732/* /End lifts to equal height script */ 1733 1734/* 1735* Serivce-chain 1736* set rows height equal 1737*/ 1738function initServiceChain(){ 1739 const serviceChains = document.querySelectorAll('.service-chain') 1740 1741 serviceChains.forEach(serviceChain => { 1742 const columns = serviceChain.querySelectorAll('.service-column') 1743 let columnEls = [] 1744 1745 // Collect targeted rows into columnEls array 1746 columns.forEach(column => { 1747 const rows = column.querySelectorAll('.bottom-section--heading, .bottom-section-links li') 1748 let rowEls = [] 1749 1750 rows.forEach(row => { 1751 row.removeAttribute('style') 1752 rowEls.push(row) 1753 }); 1754 1755 columnEls.push(rowEls) 1756 }); 1757 1758 // Fill arrays with null to make them equal length 1759 const maxLength = Math.max(...columnEls.map(arr => arr.length)); 1760 const equalLengthArrays = columnEls.map(arr => { 1761 const zerosToAdd = maxLength - arr.length; 1762 return arr.concat(new Array(zerosToAdd).fill(null)); 1763 }); 1764 1765 // Get max row height between each columns 1766 const maxRowHeightValue = equalLengthArrays[0]?.map(function(_, columnIndex) { 1767 return Math.max(...equalLengthArrays.map(row => { 1768 return row[columnIndex] !== null ? row[columnIndex].getBoundingClientRect().height : 0 1769 })); 1770 }); 1771 1772 // Set li -elements to equal height on a matching row 1773 if(maxRowHeightValue){ 1774 columnEls.forEach((col) => { 1775 col.forEach((row, index) => { 1776 if(row.getBoundingClientRect().height != maxRowHeightValue[index]){ 1777 row.style.minHeight = maxRowHeightValue[index] +'px' 1778 } 1779 }) 1780 }) 1781 } 1782 }); 1783} 1784 1785if(document.querySelectorAll('.service-chain').length > 0){ 1786 let timeId = null; 1787 const mqlTablet = window.matchMedia("(max-width: 575px)"); 1788 1789 const resize = () => { 1790 if(!mqlTablet.matches){ 1791 clearTimeout(timeId); 1792 timeId = setTimeout(initServiceChain, 300); 1793 } 1794 } 1795 1796 window.addEventListener('load', resize) 1797 window.addEventListener("resize", resize) 1798} 1799/* /End of Serivce-chain */ 1800 1801function customizeSearchSuggestionsLinks() { 1802 1803 const TARGET_CLASS = 'search-bar-suggestions-results-list'; 1804 const REMOVE_STRING = '/web/guest'; 1805 1806 let activeListElement = null; 1807 let listObserver = null; 1808 1809 const cleanLinks = (container) => { 1810 const links = container.querySelectorAll('a[href]'); 1811 1812 links.forEach(link => { 1813 const href = link.getAttribute('href'); 1814 if (href && href.includes(REMOVE_STRING)) { 1815 link.setAttribute('href', href.replace(REMOVE_STRING, '')); 1816 } 1817 }); 1818 }; 1819 1820 const startObservingList = (listElement) => { 1821 if (listObserver) { 1822 listObserver.disconnect(); 1823 } 1824 1825 activeListElement = listElement; 1826 1827 cleanLinks(listElement); 1828 1829 listObserver = new MutationObserver(() => { 1830 cleanLinks(listElement); 1831 }); 1832 1833 listObserver.observe(listElement, { 1834 childList: true, 1835 subtree: true, 1836 attributes: true 1837 }); 1838 }; 1839 1840 const stopObservingList = () => { 1841 if (listObserver) { 1842 listObserver.disconnect(); 1843 listObserver = null; 1844 } 1845 activeListElement = null; 1846 }; 1847 1848 const documentObserver = new MutationObserver(() => { 1849 const currentList = document.querySelector(`.${TARGET_CLASS}`); 1850 1851 if (!activeListElement && currentList) { 1852 startObservingList(currentList); 1853 return; 1854 } 1855 1856 if (activeListElement && !document.contains(activeListElement)) { 1857 stopObservingList(); 1858 } 1859 }); 1860 1861 documentObserver.observe(document.documentElement, { 1862 childList: true, 1863 subtree: true 1864 }); 1865 1866}
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.