1/** 2 * @package Helix Ultimate Framework 3 * @author JoomShaper https://www.joomshaper.com 4 * @copyright Copyright (c) 2010 - 2023 JoomShaper 5 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPLv2 or Later 6 */ 7 8// Preloader 9jQuery(window).on('load', function () { 10 if (jQuery('.sp-loader-with-logo').length > 0) { 11 12 move(); 13 } 14 jQuery('.sp-pre-loader').fadeOut(500, function () { 15 jQuery(this).remove(); 16 }); 17}); 18 19/** 20 * Move the progress bar 21 */ 22function move() { 23 var elem = document.getElementById('line-load'); 24 var width = 1; 25 var id = setInterval(frame, 10); 26 function frame() { 27 if (width >= 100) { 28 clearInterval(id); 29 } else { 30 width++; 31 elem.style.width = width + '%'; 32 } 33 } 34 35} 36 37jQuery(function ($) { 38 39 /** 40 * Helix settings data 41 * 42 */ 43 var settings = Joomla.getOptions('data') || {}; 44 45 /** 46 * sticky header 47 * 48 * @param {string} className the header className 49 */ 50 var handleStickiness = function (className, offsetTop) { 51 if ($('body:not(.layout-edit-iframe)').hasClass(className)) { 52 var $header = $('#sp-header'); 53 var headerHeight = $header.outerHeight(); 54 var $stickyHeaderPlaceholder = $('.sticky-header-placeholder'); 55 let $stickyOffset = '100'; 56 57 if (settings.header !== undefined && settings.header.stickyOffset !== undefined) { 58 $stickyOffset = settings.header.stickyOffset || '100'; 59 } 60 61 var stickyHeader = function () { 62 var scrollTop = $(window).scrollTop(); 63 if (scrollTop >= offsetTop + Number($stickyOffset)) { 64 $header.addClass('header-sticky'); 65 $stickyHeaderPlaceholder.height(headerHeight); 66 } else { 67 if ($header.hasClass('header-sticky')) { 68 $header.removeClass('header-sticky'); 69 $stickyHeaderPlaceholder.height('inherit'); 70 } 71 } 72 }; 73 74 stickyHeader(); 75 $(window).scroll(function () { 76 stickyHeader(); 77 }); 78 79 if ($('body').hasClass('layout-boxed')) { 80 var windowWidth = $header.parent().outerWidth(); 81 $header.css({ 'max-width': windowWidth, left: 'auto' }); 82 } 83 } else { 84 var $header = $('#sp-header'); 85 if ($header.hasClass('header-sticky')) { 86 $header.removeClass('header-sticky'); 87 } 88 $(window).off('scroll'); 89 } 90 }; 91 92 /** 93 * Calculate the header offset based on the 94 * backend preview iframe site and real site. 95 * 96 * @return integer The offset value 97 */ 98 function getHeaderOffset() { 99 /** 100 * Real site header offset top 101 * 102 */ 103 let $header = $('#sp-header'); 104 let stickyHeaderTop = $header.offset().top; 105 106 /** 107 * Backend edit preview iframe header offset top 108 * 109 */ 110 let $backHeader = $('body.back-panel').find('#sp-header'); 111 let backPanelStickyHeaderTop = null; 112 113 /** 114 * If the class .back-panel exists, 115 * that means this is from backend preview frame, 116 * then get the offset top, as it varies with the original one. 117 * 118 */ 119 if ($backHeader.length > 0) { 120 backPanelStickyHeaderTop = $backHeader.offset().top; 121 } 122 123 // By Default the header offset is the original header top 124 let headerOffset = stickyHeaderTop; 125 126 /** 127 * If back panel sticky header top has value rather than null 128 * that means the device type is changes from desktop to table or mobile or vice-versa. 129 * If value found then subtract the settings topbar height from the offset top. 130 * 131 */ 132 if (backPanelStickyHeaderTop !== null) { 133 headerOffset = backPanelStickyHeaderTop - settings.topbarHeight; 134 headerOffset = headerOffset < 0 ? stickyHeaderTop : headerOffset; 135 } 136 137 return headerOffset; 138 } 139 140 const headerExist = $('#sp-header'); 141 if (headerExist.length > 0) { 142 handleStickiness('sticky-header', getHeaderOffset()); 143 } 144 145 // go to top 146 $(window).scroll(function () { 147 if ($(this).scrollTop() > 100) { 148 $('.sp-scroll-up').fadeIn(); 149 } else { 150 $('.sp-scroll-up').fadeOut(400); 151 } 152 }); 153 154 $('.sp-scroll-up').click(function () { 155 $('html, body').animate( 156 { 157 scrollTop: -60, 158 }, 159 600 160 ); 161 return false; 162 }); 163 164 //mega menu 165 $('.sp-megamenu-wrapper').parent().parent().css('position', 'static').parent().css('position', 'relative'); 166 $('.sp-menu-full').each(function () { 167 $(this).parent().addClass('menu-justify'); 168 }); 169 170 171 172 $('#offcanvas-toggler, .offcanvas-toggler-secondary, .offcanvas-toggler-full').on('click', function (event) { 173 event.preventDefault(); 174 openOffcanvas(); 175 }); 176 177 // Close handlers 178 $('.close-offcanvas, .offcanvas-overlay').on('click', function (event) { 179 event.preventDefault(); 180 closeOffcanvas(); 181 }); 182 183 // Open function 184 function openOffcanvas() { 185 $('.offcanvas-init').addClass('offcanvas-active full-offcanvas'); 186 $(document.body).css('overflow', 'hidden'); 187 188 // Make offcanvas interactive 189 $('.offcanvas-menu') 190 .removeAttr('inert') 191 .attr('tabindex', '0'); 192 193 // Set focus to close button (better for accessibility) 194 setTimeout(() => { 195 $('.close-offcanvas').focus(); 196 }, 100); 197 198 // Add keyboard trap 199 $(document).on('keydown.offcanvas', handleOffcanvasKeyboard); 200 } 201 202 // Close function 203 function closeOffcanvas() { 204 $('.offcanvas-init').removeClass('offcanvas-active full-offcanvas'); 205 $(document.body).css('overflow', ''); 206 207 // Make offcanvas non-interactive 208 $('.offcanvas-menu') 209 .attr('inert', '') 210 .attr('tabindex', '-1'); 211 212 // Return focus to the trigger 213 $('#offcanvas-toggler').focus(); 214 215 // Remove keyboard trap 216 $(document).off('keydown.offcanvas'); 217 } 218 219 // Keyboard trap handler on offcanvas 220 function handleOffcanvasKeyboard(e) { 221 if (!$('.offcanvas-init').hasClass('offcanvas-active')) return; 222 223 const $offcanvas = $('.offcanvas-menu'); 224 const focusable = 'a[href], button, input, textarea, select, [tabindex="0"]'; 225 const $focusable = $offcanvas.find(focusable).filter(':visible'); 226 227 // Escape key closes 228 if (e.key === 'Escape') { 229 e.preventDefault(); 230 closeOffcanvas(); 231 return; 232 } 233 234 // Tab key containment 235 if (e.key === 'Tab') { 236 const $first = $focusable.first(); 237 const $last = $focusable.last(); 238 239 if (!$offcanvas[0].contains(document.activeElement)) { 240 e.preventDefault(); 241 $first.focus(); 242 return; 243 } 244 245 if (e.shiftKey && document.activeElement === $first[0]) { 246 e.preventDefault(); 247 $last.focus(); 248 } else if (!e.shiftKey && document.activeElement === $last[0]) { 249 e.preventDefault(); 250 $first.focus(); 251 } 252 } 253 } 254 255 // Prevent focus on inert elements 256 $(document).on('focusin', function (e) { 257 if ($(e.target).closest('[inert]').length) { 258 e.preventDefault(); 259 $('#offcanvas-toggler').focus(); 260 } 261 }); 262 263 // Load inert polyfill if needed 264 if (!('inert' in document.createElement('div'))) { 265 const inertPolyfill = document.createElement('script'); 266 inertPolyfill.src = 'https://cdn.jsdelivr.net/npm/[email protected]/inert.min.js'; 267 document.head.appendChild(inertPolyfill); 268 } 269 270 // Modal Menu
271 if ($('#modal-menu').length > 0) { 272 let $modalToggler = $('#modal-menu-toggler'); 273 let $modalMenu = $('#modal-menu'); 274 let $body = $('body'); 275 276 $modalToggler.on('click', function (e) { 277 e.preventDefault(); 278 $modalMenu.toggleClass('active'); 279 $body.toggleClass('modal-menu-active'); 280 $(this).toggleClass('active'); 281 }); 282 283 // modal menu close with escape 284 $(document).keyup(function (e) { 285 if (e.key == 'Escape') { 286 $modalMenu.removeClass('active'); 287 $modalToggler.removeClass('active'); 288 $body.removeClass('modal-menu-active'); 289 } 290 }); 291 } 292 293 // Tooltip 294 const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"], .hasTooltip')); 295 tooltipTriggerList.map(function (tooltipTriggerEl) { 296 return new bootstrap.Tooltip(tooltipTriggerEl, { 297 html: true 298 }); 299 }); 300 301 // Popover 302 const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"], .hasPopover')); 303 popoverTriggerList.map(function (popoverTriggerEl) { 304 return new bootstrap.Popover(popoverTriggerEl); 305 }); 306 307 // Article Ajax voting 308 $('.article-ratings .rating-star').on('click', function (event) { 309 event.preventDefault(); 310 var $parent = $(this).closest('.article-ratings'); 311 312 var request = { 313 option: 'com_ajax', 314 template: template, 315 action: 'rating', 316 rating: $(this).data('number'), 317 article_id: $parent.data('id'), 318 format: 'json', 319 }; 320 321 $.ajax({ 322 type: 'POST', 323 data: request, 324 beforeSend: function () { 325 $parent.find('.fa-spinner').show(); 326 }, 327 success: function (response) { 328 var data = $.parseJSON(response); 329 $parent.find('.ratings-count').text(data.message); 330 $parent.find('.fa-spinner').hide(); 331 332 if (data.status) { 333 $parent.find('.rating-symbol').html(data.ratings); 334 } 335 336 setTimeout(function () { 337 $parent.find('.ratings-count').text('(' + data.rating_count + ')'); 338 }, 3000); 339 }, 340 }); 341 }); 342 343 // Cookie consent 344 $('.sp-cookie-allow').on('click', function (event) { 345 event.preventDefault(); 346 347 var date = new Date(); 348 date.setTime(date.getTime() + 30 * 24 * 60 * 60 * 1000); 349 var expires = '; expires=' + date.toGMTString(); 350 document.cookie = 'spcookie_status=ok' + expires + '; path=/'; 351 352 $(this).closest('.sp-cookie-consent').fadeOut(); 353 }); 354 355 $('.btn-group label:not(.active)').click(function () { 356 var label = $(this); 357 var input = $('#' + label.attr('for')); 358 359 if (!input.prop('checked')) { 360 label.closest('.btn-group').find('label').removeClass('active btn-success btn-danger btn-primary'); 361 if (input.val() === '') { 362 label.addClass('active btn-primary'); 363 } else if (input.val() == 0) { 364 label.addClass('active btn-danger'); 365 } else { 366 label.addClass('active btn-success'); 367 } 368 input.prop('checked', true); 369 input.trigger('change'); 370 } 371 var parent = $(this).parents('#attrib-helix_ultimate_blog_options'); 372 if (parent) { 373 showCategoryItems(parent, input.val()); 374 } 375 }); 376 $('.btn-group input[checked=checked]').each(function () { 377 if ($(this).val() == '') { 378 $('label[for=' + $(this).attr('id') + ']').addClass('active btn btn-primary'); 379 } else if ($(this).val() == 0) { 380 $('label[for=' + $(this).attr('id') + ']').addClass('active btn btn-danger'); 381 } else { 382 $('label[for=' + $(this).attr('id') + ']').addClass('active btn btn-success'); 383 } 384 var parent = $(this).parents('#attrib-helix_ultimate_blog_options'); 385 if (parent) { 386 parent.find('*[data-showon]').each(function () { 387 $(this).hide(); 388 }); 389 } 390 }); 391 392 function showCategoryItems(parent, value) { 393 var controlGroup = parent.find('*[data-showon]'); 394 395 controlGroup.each(function () { 396 var data = $(this).attr('data-showon'); 397 data = typeof data !== 'undefined' ? JSON.parse(data) : []; 398 if (data.length > 0) { 399 if (typeof data[0].values !== 'undefined' && data[0].values.includes(value)) { 400 $(this).slideDown(); 401 } else { 402 $(this).hide(); 403 } 404 } 405 }); 406 } 407 408 $(window).on('scroll', function () { 409 var scrollBar = $('.sp-reading-progress-bar'); 410 if (scrollBar.length > 0) { 411 var s = $(window).scrollTop(), 412 d = $(document).height(), 413 c = $(window).height(); 414 var scrollPercent = (s / (d - c)) * 100; 415 const position = scrollBar.data('position'); 416 if (position === 'top') { 417 // var sticky = $('.header-sticky'); 418 // if( sticky.length > 0 ){ 419 // sticky.css({ top: scrollBar.height() }) 420 // }else{ 421 // sticky.css({ top: 0 }) 422 // } 423 } 424 scrollBar.css({ width: `${scrollPercent}%` }); 425 } 426 }); 427 428 // Error Alert close issue fix for Joomla 3 429 var observer = new MutationObserver(function (mutations) { 430 $('#system-message-container .alert .close').attr('data-bs-dismiss', 'alert'); 431 }); 432 var target = document.querySelector('#system-message-container'); 433 observer.observe(target, { 434 attributes: true 435 }); 436 437 438}); 439 440 441// Handle accessibility on off-canvas dropdown menus 442jQuery(function ($) { 443 const menuSelector = '.menu-deeper.menu-parent'; 444 const togglerSelector = '.menu-toggler'; 445 const childSelector = '.menu-child'; 446 447 // Toggle submenu open/close 448 function toggleSubmenu($item, open = null) { 449 const $submenu = $item.children(childSelector); 450 const isOpen = $item.hasClass('menu-parent-open'); 451 452 if (open === null) open = !isOpen; 453 454 if (open) { 455 $item.addClass('menu-parent-open').attr('aria-expanded', 'true'); 456 $submenu.slideDown(150); 457 } else { 458 $item.removeClass('menu-parent-open').attr('aria-expanded', 'false');
459 $submenu.slideUp(150); 460 } 461 } 462 463 // Prevent event bubbling from toggler to link 464 $(document).on('click', togglerSelector, function (event) { 465 event.preventDefault(); 466 event.stopPropagation(); 467 const $item = $(this).closest(menuSelector); 468 const isOpen = $item.hasClass('menu-parent-open'); 469 toggleSubmenu($item, !isOpen); 470 }); 471 472 // Handle menu link click or Enter/Space key 473 $(document).on('click keydown', `${menuSelector} > a`, function (event) { 474 const isToggleKey = event.type === 'click' || event.key === 'Enter' || event.key === ' '; 475 const isKeyboard = event.key === 'Enter' || event.key === ' '; 476 477 if (isToggleKey) { 478 const $item = $(this).closest(menuSelector); 479 const $submenu = $item.children(childSelector); 480 481 // If submenu exists 482 if ($submenu.length) { 483 // If it's Space key (prevent scrolling), toggle submenu only 484 if (event.key === ' ') { 485 event.preventDefault(); 486 toggleSubmenu($item); 487 } 488 // If it's Enter or click â follow link (do not preventDefault) 489 // but also toggle submenu if desired 490 else if (event.key === 'Enter') { 491 toggleSubmenu($item, true); 492 493 } else if (event.type === 'click') { 494 toggleSubmenu($item, true); 495 } 496 } 497 } 498 }); 499 500 // Handle arrow key navigation 501 $(document).on('keydown', `${menuSelector} > a`, function (event) { 502 const $currentItem = $(this).closest('li'); 503 const $siblings = $currentItem.parent().children('li:visible'); 504 const index = $siblings.index($currentItem); 505 506 if (event.key === 'ArrowDown') { 507 event.preventDefault(); 508 // If submenu is open, focus first child link 509 const $submenu = $currentItem.children(childSelector); 510 if ($submenu.length && $currentItem.hasClass('menu-parent-open')) { 511 const $firstChildLink = $submenu.children('li:visible').find('a').first(); 512 if ($firstChildLink.length) { 513 $firstChildLink.focus(); 514 return; 515 } 516 } 517 // Otherwise, move to next sibling 518 $siblings.eq((index + 1) % $siblings.length).find('a').first().focus(); 519 } else if (event.key === 'ArrowUp') { 520 event.preventDefault(); 521 // Move to previous sibling 522 $siblings.eq((index - 1 + $siblings.length) % $siblings.length).find('a').first().focus(); 523 } else if (event.key === 'ArrowRight') { 524 // Open nested submenu if present 525 const $submenu = $currentItem.children(childSelector); 526 if ($submenu.length) { 527 event.preventDefault(); 528 toggleSubmenu($currentItem, true); 529 const $firstChildLink = $submenu.children('li:visible').find('a').first(); 530 if ($firstChildLink.length) { 531 $firstChildLink.focus(); 532 } 533 } 534 } else if (event.key === 'ArrowLeft' || event.key === 'Escape') { 535 event.preventDefault(); 536 // Close only the current open dropdown 537 toggleSubmenu($currentItem, false); 538 $currentItem.children('a').first().focus(); 539 } 540 }); 541 542 // Handle arrow key navigation inside submenu 543 $(document).on('keydown', `${childSelector} > li > a`, function (event) { 544 const $currentItem = $(this).closest('li'); 545 const $siblings = $currentItem.parent().children('li:visible'); 546 const index = $siblings.index($currentItem); 547 548 if (event.key === 'ArrowDown') { 549 event.preventDefault(); 550 $siblings.eq((index + 1) % $siblings.length).find('a').first().focus(); 551 } else if (event.key === 'ArrowUp') { 552 event.preventDefault(); 553 $siblings.eq((index - 1 + $siblings.length) % $siblings.length).find('a').first().focus(); 554 } else if (event.key === 'ArrowRight') { 555 // Open nested submenu if present 556 const $submenu = $currentItem.children(childSelector); 557 if ($submenu.length) { 558 event.preventDefault(); 559 toggleSubmenu($currentItem, true); 560 const $firstChildLink = $submenu.children('li:visible').find('a').first(); 561 if ($firstChildLink.length) { 562 $firstChildLink.focus(); 563 } 564 } 565 } else if (event.key === 'ArrowLeft' || event.key === 'Escape') { 566 event.preventDefault(); 567 // Close submenu and focus parent 568 const $parentMenu = $currentItem.parents(menuSelector).first(); 569 toggleSubmenu($parentMenu, false); 570 $parentMenu.children('a').first().focus(); 571 } 572 }); 573 574 // Close all menus when clicking outside 575 $(document).on('click', function (event) { 576 if (!$(event.target).closest('.menu-deeper').length) { 577 $(menuSelector).removeClass('menu-parent-open').attr('aria-expanded', 'false');
578 $(childSelector).slideUp(150); 579 } 580 }); 581 582 //mark all menu-parents as aria-haspopup 583 $(menuSelector).attr('aria-haspopup', 'true').attr('aria-expanded', 'false'); 584}); 585 586// Handle accessibility on megamenu and profile dropdowns 587jQuery(function ($) { 588 const menuSelectors = '.sp-megamenu-parent > li, .sp-profile-wrapper'; 589 590 $(menuSelectors).each(function () { 591 const $menuItem = $(this); 592 const $trigger = $menuItem.children('a, button'); 593 const $dropdown = $menuItem.children('.sp-dropdown, .sp-profile-dropdown'); 594 595 if ($dropdown.length) { 596 setupDropdownEvents($menuItem, $trigger, $dropdown); 597 } 598 }); 599 600 bindNestedDropdowns('body'); 601 602 function setupDropdownEvents($menuItem, $trigger, $dropdown) { 603 // Show on focus or mouseenter 604 $trigger.on('focus mouseenter', function () { 605 openMenu($menuItem, $dropdown); 606 }); 607 608 $menuItem.on('mouseenter', function () { 609 openMenu($menuItem, $dropdown); 610 }); 611 612 // Hide on focusout or mouseleave 613 $menuItem.on('mouseleave focusout', function () { 614 setTimeout(function () { 615 if (!$menuItem.find(':focus').length && !$menuItem.is(':hover')) { 616 closeMenu($menuItem, $dropdown); 617 } 618 }, 100); 619 }); 620 621 // Keyboard trigger 622 $trigger.on('keydown', function (event) { 623 switch (event.key) { 624 case ' ': 625 event.preventDefault(); 626 openMenu($menuItem, $dropdown); 627 break; 628 case 'ArrowDown': 629 event.preventDefault(); 630 openMenu($menuItem, $dropdown); 631 focusFirstItem($dropdown); 632 break; 633 case 'Escape': 634 event.preventDefault(); 635 closeMenu($menuItem, $dropdown); 636 $trigger.focus(); 637 break; 638 } 639 }); 640 641 // Prevent click from toggling menu 642 $trigger.on('click', function (event) { 643 if ($dropdown.length) { 644 openMenu($menuItem, $dropdown); 645 } 646 }); 647 648 } 649 650 function bindNestedDropdowns(containerSelector) { 651 $(containerSelector).find(' .sp-has-child').each(function () { 652 const $subItem = $(this); 653 const $trigger = $subItem.children('a, button'); 654 const $subDropdown = $subItem.children('.sp-dropdown'); 655 656 if ($subDropdown.length) { 657 setupDropdownEvents($subItem, $trigger, $subDropdown); 658 bindNestedDropdowns($subDropdown); 659 } 660 }); 661 } 662 663 function openMenu($item, $dropdown) { 664 $dropdown.show(); 665 // Only force display for .sp-profile-dropdown 666 if ($dropdown.hasClass('sp-profile-dropdown')) { 667 $dropdown.attr('style', 'display: block !important'); 668 } 669 bindKeyboardNavigation($dropdown); 670 } 671 672 function closeMenu($item, $dropdown) { 673 $dropdown.hide(); 674 // Reset style for .sp-profile-dropdown 675 if ($dropdown.hasClass('sp-profile-dropdown')) { 676 $dropdown.removeAttr('style'); 677 } 678 } 679 680 function focusFirstItem($dropdown) { 681 const $focusable = $dropdown.find('a, button').filter(':visible'); 682 if ($focusable.length) { 683 $focusable.first().focus(); 684 } 685 } 686 687 function bindKeyboardNavigation($dropdown) { 688 const $items = $dropdown.find('a, button').filter(':visible'); 689 690 $items.off('keydown').on('keydown', function (event) { 691 const currentIndex = $items.index(this); 692 let newIndex = -1; 693 694 if (event.key === 'ArrowDown') { 695 event.preventDefault(); 696 newIndex = (currentIndex + 1) % $items.length; 697 } else if (event.key === 'ArrowUp') { 698 event.preventDefault(); 699 newIndex = (currentIndex - 1 + $items.length) % $items.length; 700 } else if (event.key === 'Escape') { 701 event.preventDefault(); 702 703 // Reset style for .sp-profile-dropdown 704 if ($dropdown.hasClass('sp-profile-dropdown')) { 705 $dropdown.removeAttr('style'); 706 } 707 708 const $currentDropdown = $(this).closest('.sp-dropdown'); 709 const $parentItem = $currentDropdown.parent(' .sp-has-child, .sp-megamenu-parent > li'); 710 711 // Check if this is the root-level dropdown 712 const isRoot = $parentItem.parent().is('.sp-megamenu-parent, .sp-megamenu-parent > ul, nav'); 713 714 if (isRoot) { 715 // Close all menus 716 $(menuSelectors).each(function () { 717 const $item = $(this); 718 closeMenu($item, $item.children('.sp-dropdown')); 719 }); 720 } else { 721 // Close only current submenu and focus its trigger 722 const $trigger = $parentItem.children('a, button'); 723 closeMenu($parentItem, $currentDropdown); 724 if ($trigger.length) { 725 $trigger.focus(); 726 } 727 } 728 return; 729 } 730 731 if (newIndex > -1) { 732 $items.eq(newIndex).focus(); 733 } 734 }); 735 } 736 737 // Close all menus on outside click
738 $(document).on('click', function (event) { 739 if (!$(event.target).closest(menuSelectors).length) { 740 $(menuSelectors).each(function () { 741 const $item = $(this); 742 closeMenu($item, $item.children('.sp-dropdown')); 743 }); 744 } 745 }); 746 747}); 748 749
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.