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 // Modal Menu
264 if ($('#modal-menu').length > 0) { 265 let $modalToggler = $('#modal-menu-toggler'); 266 let $modalMenu = $('#modal-menu'); 267 let $body = $('body'); 268 269 $modalToggler.on('click', function (e) { 270 e.preventDefault(); 271 $modalMenu.toggleClass('active'); 272 $body.toggleClass('modal-menu-active'); 273 $(this).toggleClass('active'); 274 }); 275 276 // modal menu close with escape 277 $(document).keyup(function (e) { 278 if (e.key == 'Escape') { 279 $modalMenu.removeClass('active'); 280 $modalToggler.removeClass('active'); 281 $body.removeClass('modal-menu-active'); 282 } 283 }); 284 } 285 286 // Tooltip 287 const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"], .hasTooltip')); 288 tooltipTriggerList.map(function (tooltipTriggerEl) { 289 return new bootstrap.Tooltip(tooltipTriggerEl, { 290 html: true 291 }); 292 }); 293 294 // Popover 295 const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"], .hasPopover')); 296 popoverTriggerList.map(function (popoverTriggerEl) { 297 return new bootstrap.Popover(popoverTriggerEl); 298 }); 299 300 // Article Ajax voting 301 $('.article-ratings .rating-star').on('click', function (event) { 302 event.preventDefault(); 303 var $parent = $(this).closest('.article-ratings'); 304 305 var request = { 306 option: 'com_ajax', 307 template: template, 308 action: 'rating', 309 rating: $(this).data('number'), 310 article_id: $parent.data('id'), 311 format: 'json', 312 }; 313 314 $.ajax({ 315 type: 'POST', 316 data: request, 317 beforeSend: function () { 318 $parent.find('.fa-spinner').show(); 319 }, 320 success: function (response) { 321 var data = $.parseJSON(response); 322 $parent.find('.ratings-count').text(data.message); 323 $parent.find('.fa-spinner').hide(); 324 325 if (data.status) { 326 $parent.find('.rating-symbol').html(data.ratings); 327 } 328 329 setTimeout(function () { 330 $parent.find('.ratings-count').text('(' + data.rating_count + ')'); 331 }, 3000); 332 }, 333 }); 334 }); 335 336 // Cookie consent 337 $('.sp-cookie-allow').on('click', function (event) { 338 event.preventDefault(); 339 340 var date = new Date(); 341 date.setTime(date.getTime() + 30 * 24 * 60 * 60 * 1000); 342 var expires = '; expires=' + date.toGMTString(); 343 document.cookie = 'spcookie_status=ok' + expires + '; path=/'; 344 345 $(this).closest('.sp-cookie-consent').fadeOut(); 346 }); 347 348 $('.btn-group label:not(.active)').click(function () { 349 var label = $(this); 350 var input = $('#' + label.attr('for')); 351 352 if (!input.prop('checked')) { 353 label.closest('.btn-group').find('label').removeClass('active btn-success btn-danger btn-primary'); 354 if (input.val() === '') { 355 label.addClass('active btn-primary'); 356 } else if (input.val() == 0) { 357 label.addClass('active btn-danger'); 358 } else { 359 label.addClass('active btn-success'); 360 } 361 input.prop('checked', true); 362 input.trigger('change'); 363 } 364 var parent = $(this).parents('#attrib-helix_ultimate_blog_options'); 365 if (parent) { 366 showCategoryItems(parent, input.val()); 367 } 368 }); 369 $('.btn-group input[checked=checked]').each(function () { 370 if ($(this).val() == '') { 371 $('label[for=' + $(this).attr('id') + ']').addClass('active btn btn-primary'); 372 } else if ($(this).val() == 0) { 373 $('label[for=' + $(this).attr('id') + ']').addClass('active btn btn-danger'); 374 } else { 375 $('label[for=' + $(this).attr('id') + ']').addClass('active btn btn-success'); 376 } 377 var parent = $(this).parents('#attrib-helix_ultimate_blog_options'); 378 if (parent) { 379 parent.find('*[data-showon]').each(function () { 380 $(this).hide(); 381 }); 382 } 383 }); 384 385 function showCategoryItems(parent, value) { 386 var controlGroup = parent.find('*[data-showon]'); 387 388 controlGroup.each(function () { 389 var data = $(this).attr('data-showon'); 390 data = typeof data !== 'undefined' ? JSON.parse(data) : []; 391 if (data.length > 0) { 392 if (typeof data[0].values !== 'undefined' && data[0].values.includes(value)) { 393 $(this).slideDown(); 394 } else { 395 $(this).hide(); 396 } 397 } 398 }); 399 } 400 401 $(window).on('scroll', function () { 402 var scrollBar = $('.sp-reading-progress-bar'); 403 if (scrollBar.length > 0) { 404 var s = $(window).scrollTop(), 405 d = $(document).height(), 406 c = $(window).height(); 407 var scrollPercent = (s / (d - c)) * 100; 408 const position = scrollBar.data('position'); 409 if (position === 'top') { 410 // var sticky = $('.header-sticky'); 411 // if( sticky.length > 0 ){ 412 // sticky.css({ top: scrollBar.height() }) 413 // }else{ 414 // sticky.css({ top: 0 }) 415 // } 416 } 417 scrollBar.css({ width: `${scrollPercent}%` }); 418 } 419 }); 420 421 // Error Alert close issue fix for Joomla 3 422 var observer = new MutationObserver(function (mutations) { 423 $('#system-message-container .alert .close').attr('data-bs-dismiss', 'alert'); 424 }); 425 var target = document.querySelector('#system-message-container'); 426 observer.observe(target, { 427 attributes: true 428 }); 429 430 431}); 432 433 434// Handle accessibility on off-canvas dropdown menus 435jQuery(function ($) { 436 const menuSelector = '.menu-deeper.menu-parent'; 437 const togglerSelector = '.menu-toggler'; 438 const childSelector = '.menu-child'; 439 440 // Toggle submenu open/close 441 function toggleSubmenu($item, open = null) { 442 const $submenu = $item.children(childSelector); 443 const isOpen = $item.hasClass('menu-parent-open'); 444 445 if (open === null) open = !isOpen; 446 447 if (open) { 448 $item.addClass('menu-parent-open').attr('aria-expanded', 'true'); 449 $submenu.slideDown(150); 450 } else { 451 $item.removeClass('menu-parent-open').attr('aria-expanded', 'false');
452 $submenu.slideUp(150); 453 } 454 } 455 456 // Prevent event bubbling from toggler to link 457 $(document).on('click', togglerSelector, function (event) { 458 event.preventDefault(); 459 event.stopPropagation(); 460 const $item = $(this).closest(menuSelector); 461 const isOpen = $item.hasClass('menu-parent-open'); 462 toggleSubmenu($item, !isOpen); 463 }); 464 465 // Handle menu link click or Enter/Space key 466 $(document).on('click keydown', `${menuSelector} > a`, function (event) { 467 const isToggleKey = event.type === 'click' || event.key === 'Enter' || event.key === ' '; 468 const isKeyboard = event.key === 'Enter' || event.key === ' '; 469 470 if (isToggleKey) { 471 const $item = $(this).closest(menuSelector); 472 const $submenu = $item.children(childSelector); 473 474 // If submenu exists 475 if ($submenu.length) { 476 // If it's Space key (prevent scrolling), toggle submenu only 477 if (event.key === ' ') { 478 event.preventDefault(); 479 toggleSubmenu($item); 480 } 481 // If it's Enter or click â follow link (do not preventDefault) 482 // but also toggle submenu if desired 483 else if (event.key === 'Enter') { 484 toggleSubmenu($item, true); 485 486 } else if (event.type === 'click') { 487 toggleSubmenu($item, true); 488 } 489 } 490 } 491 }); 492 493 // Handle arrow key navigation 494 $(document).on('keydown', `${menuSelector} > a`, function (event) { 495 const $currentItem = $(this).closest('li'); 496 const $siblings = $currentItem.parent().children('li:visible'); 497 const index = $siblings.index($currentItem); 498 499 if (event.key === 'ArrowDown') { 500 event.preventDefault(); 501 // If submenu is open, focus first child link 502 const $submenu = $currentItem.children(childSelector); 503 if ($submenu.length && $currentItem.hasClass('menu-parent-open')) { 504 const $firstChildLink = $submenu.children('li:visible').find('a').first(); 505 if ($firstChildLink.length) { 506 $firstChildLink.focus(); 507 return; 508 } 509 } 510 // Otherwise, move to next sibling 511 $siblings.eq((index + 1) % $siblings.length).find('a').first().focus(); 512 } else if (event.key === 'ArrowUp') { 513 event.preventDefault(); 514 // Move to previous sibling 515 $siblings.eq((index - 1 + $siblings.length) % $siblings.length).find('a').first().focus(); 516 } else if (event.key === 'ArrowRight') { 517 // Open nested submenu if present 518 const $submenu = $currentItem.children(childSelector); 519 if ($submenu.length) { 520 event.preventDefault(); 521 toggleSubmenu($currentItem, true); 522 const $firstChildLink = $submenu.children('li:visible').find('a').first(); 523 if ($firstChildLink.length) { 524 $firstChildLink.focus(); 525 } 526 } 527 } else if (event.key === 'ArrowLeft' || event.key === 'Escape') { 528 event.preventDefault(); 529 // Close only the current open dropdown 530 toggleSubmenu($currentItem, false); 531 $currentItem.children('a').first().focus(); 532 } 533 }); 534 535 // Handle arrow key navigation inside submenu 536 $(document).on('keydown', `${childSelector} > li > a`, function (event) { 537 const $currentItem = $(this).closest('li'); 538 const $siblings = $currentItem.parent().children('li:visible'); 539 const index = $siblings.index($currentItem); 540 541 if (event.key === 'ArrowDown') { 542 event.preventDefault(); 543 $siblings.eq((index + 1) % $siblings.length).find('a').first().focus(); 544 } else if (event.key === 'ArrowUp') { 545 event.preventDefault(); 546 $siblings.eq((index - 1 + $siblings.length) % $siblings.length).find('a').first().focus(); 547 } else if (event.key === 'ArrowRight') { 548 // Open nested submenu if present 549 const $submenu = $currentItem.children(childSelector); 550 if ($submenu.length) { 551 event.preventDefault(); 552 toggleSubmenu($currentItem, true); 553 const $firstChildLink = $submenu.children('li:visible').find('a').first(); 554 if ($firstChildLink.length) { 555 $firstChildLink.focus(); 556 } 557 } 558 } else if (event.key === 'ArrowLeft' || event.key === 'Escape') { 559 event.preventDefault(); 560 // Close submenu and focus parent 561 const $parentMenu = $currentItem.parents(menuSelector).first(); 562 toggleSubmenu($parentMenu, false); 563 $parentMenu.children('a').first().focus(); 564 } 565 }); 566 567 // Close all menus when clicking outside 568 $(document).on('click', function (event) { 569 if (!$(event.target).closest('.menu-deeper').length) { 570 $(menuSelector).removeClass('menu-parent-open').attr('aria-expanded', 'false');
571 $(childSelector).slideUp(150); 572 } 573 }); 574 575 //mark all menu-parents as aria-haspopup 576 $(menuSelector).attr('aria-haspopup', 'true').attr('aria-expanded', 'false'); 577}); 578 579// Handle accessibility on megamenu and profile dropdowns 580jQuery(function ($) { 581 const menuSelectors = '.sp-megamenu-parent > li, .sp-profile-wrapper'; 582 583 $(menuSelectors).each(function () { 584 const $menuItem = $(this); 585 const $trigger = $menuItem.children('a, button'); 586 const $dropdown = $menuItem.children('.sp-dropdown, .sp-profile-dropdown'); 587 588 if ($dropdown.length) { 589 setupDropdownEvents($menuItem, $trigger, $dropdown); 590 } 591 }); 592 593 bindNestedDropdowns('body'); 594 595 function setupDropdownEvents($menuItem, $trigger, $dropdown) { 596 // Show on focus or mouseenter 597 $trigger.on('focus mouseenter', function () { 598 openMenu($menuItem, $dropdown); 599 }); 600 601 $menuItem.on('mouseenter', function () { 602 openMenu($menuItem, $dropdown); 603 }); 604 605 // Hide on focusout or mouseleave 606 $menuItem.on('mouseleave focusout', function () { 607 setTimeout(function () { 608 if (!$menuItem.find(':focus').length && !$menuItem.is(':hover')) { 609 closeMenu($menuItem, $dropdown); 610 } 611 }, 100); 612 }); 613 614 // Keyboard trigger 615 $trigger.on('keydown', function (event) { 616 switch (event.key) { 617 case ' ': 618 event.preventDefault(); 619 openMenu($menuItem, $dropdown); 620 break; 621 case 'ArrowDown': 622 event.preventDefault(); 623 openMenu($menuItem, $dropdown); 624 focusFirstItem($dropdown); 625 break; 626 case 'Escape': 627 event.preventDefault(); 628 closeMenu($menuItem, $dropdown); 629 $trigger.focus(); 630 break; 631 } 632 }); 633 634 // Prevent click from toggling menu 635 $trigger.on('click', function (event) { 636 if ($dropdown.length) { 637 openMenu($menuItem, $dropdown); 638 } 639 }); 640 641 } 642 643 function bindNestedDropdowns(containerSelector) { 644 $(containerSelector).find(' .sp-has-child').each(function () { 645 const $subItem = $(this); 646 const $trigger = $subItem.children('a, button'); 647 const $subDropdown = $subItem.children('.sp-dropdown'); 648 649 if ($subDropdown.length) { 650 setupDropdownEvents($subItem, $trigger, $subDropdown); 651 bindNestedDropdowns($subDropdown); 652 } 653 }); 654 } 655 656 function openMenu($item, $dropdown) { 657 $dropdown.show(); 658 // Only force display for .sp-profile-dropdown 659 if ($dropdown.hasClass('sp-profile-dropdown')) { 660 $dropdown.attr('style', 'display: block !important'); 661 } 662 bindKeyboardNavigation($dropdown); 663 } 664 665 function closeMenu($item, $dropdown) { 666 $dropdown.hide(); 667 // Reset style for .sp-profile-dropdown 668 if ($dropdown.hasClass('sp-profile-dropdown')) { 669 $dropdown.removeAttr('style'); 670 } 671 } 672 673 function focusFirstItem($dropdown) { 674 const $focusable = $dropdown.find('a, button').filter(':visible'); 675 if ($focusable.length) { 676 $focusable.first().focus(); 677 } 678 } 679 680 function bindKeyboardNavigation($dropdown) { 681 const $items = $dropdown.find('a, button').filter(':visible'); 682 683 $items.off('keydown').on('keydown', function (event) { 684 const currentIndex = $items.index(this); 685 let newIndex = -1; 686 687 if (event.key === 'ArrowDown') { 688 event.preventDefault(); 689 newIndex = (currentIndex + 1) % $items.length; 690 } else if (event.key === 'ArrowUp') { 691 event.preventDefault(); 692 newIndex = (currentIndex - 1 + $items.length) % $items.length; 693 } else if (event.key === 'Escape') { 694 event.preventDefault(); 695 696 // Reset style for .sp-profile-dropdown 697 if ($dropdown.hasClass('sp-profile-dropdown')) { 698 $dropdown.removeAttr('style'); 699 } 700 701 const $currentDropdown = $(this).closest('.sp-dropdown'); 702 const $parentItem = $currentDropdown.parent(' .sp-has-child, .sp-megamenu-parent > li'); 703 704 // Check if this is the root-level dropdown 705 const isRoot = $parentItem.parent().is('.sp-megamenu-parent, .sp-megamenu-parent > ul, nav'); 706 707 if (isRoot) { 708 // Close all menus 709 $(menuSelectors).each(function () { 710 const $item = $(this); 711 closeMenu($item, $item.children('.sp-dropdown')); 712 }); 713 } else { 714 // Close only current submenu and focus its trigger 715 const $trigger = $parentItem.children('a, button'); 716 closeMenu($parentItem, $currentDropdown); 717 if ($trigger.length) { 718 $trigger.focus(); 719 } 720 } 721 return; 722 } 723 724 if (newIndex > -1) { 725 $items.eq(newIndex).focus(); 726 } 727 }); 728 } 729 730 // Close all menus on outside click
731 $(document).on('click', function (event) { 732 if (!$(event.target).closest(menuSelectors).length) { 733 $(menuSelectors).each(function () { 734 const $item = $(this); 735 closeMenu($item, $item.children('.sp-dropdown')); 736 }); 737 } 738 }); 739 740}); 741 742
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.