1/** 2 * Table of Contents: 3 * 1. Custom Videos 4 * 2. Sticky Menu(On Scrolling Up) 5 * 3. Back To Top 6 * 4. Media Slider 7 * 5. Mansory Slider 8 * 6. Logos Slider 9 * 7. FAQs Accordion 10 * 8. Testimonials Slider 11 * 9. Tabs Accordion 12 * 10. Back To Top 13 * 11. Team Member Lightbox 14 * 12. Load More Press Release 15 * 13. Load More Reviews 16 * 14. Load More Case Studies 17 * 15. Counters 18 * 16. Post Load More 19 * 17. Post Sticky Sidebar 20 * 18. Compare Table Slider 21 * 19. Sticky Pricing Table 22 */ 23 24/*-------------------------------------------------------------- 25# Custom Videos 26--------------------------------------------------------------*/ 27 28document.addEventListener('DOMContentLoaded', function () { 29 // Initialize Plyr for regular video, YouTube, and Vimeo players 30 document.querySelectorAll('.player').forEach((p) => { 31 const player = new Plyr(p, { 32 fullscreen: { enabled: true, fallback: true, iosNative: true } 33 }); 34 35 // Enable fullscreen button manually if needed 36 p.addEventListener('dblclick', () => { 37 player.fullscreen.enter(); 38 }); 39 }); 40 41 // Initialize Wistia players with Plyr 42 document.querySelectorAll('.wistia-player').forEach((element) => { 43 const wistiaId = element.dataset.wistiaId; 44 45 // Create iframe for Wistia 46 const iframe = document.createElement('iframe'); 47 iframe.src = `https://fast.wistia.net/embed/iframe/${wistiaId}`; 48 iframe.allowFullscreen = true; 49 iframe.allow = 'autoplay; fullscreen'; 50 51 // Add poster if available 52 if (element.dataset.poster) { 53 iframe.dataset.poster = element.dataset.poster; 54 } 55 56 element.appendChild(iframe); 57 58 // Initialize Plyr on the Wistia iframe 59 const player = new Plyr(iframe, { 60 fullscreen: { enabled: true, fallback: true, iosNative: true } 61 }); 62 63 // Enable fullscreen on double-click 64 iframe.addEventListener('dblclick', () => { 65 player.fullscreen.enter(); 66 }); 67 }); 68}); 69 70/*-------------------------------------------------------------- 71# Sticky Menu(On Scrolling Up) 72--------------------------------------------------------------*/ 73jQuery(function () { 74 var header = document.querySelector('.site-header'); 75 var ticking = false; 76 77 window.addEventListener('scroll', function() { 78 if (!ticking) { 79 requestAnimationFrame(function() { 80 var st = window.scrollY || window.pageYOffset; 81 if (st > 80) { 82 header.classList.add('header-scrolled'); 83 } else { 84 header.classList.remove('header-scrolled'); 85 } 86 ticking = false; 87 }); 88 ticking = true; 89 } 90 }, { passive: true }); 91}); 92 93 94/*-------------------------------------------------------------- 95# Back To Top 96--------------------------------------------------------------*/ 97document.addEventListener('DOMContentLoaded', function () { 98 "use strict"; 99 100 var progressPath = document.querySelector('.progress-wrap path'); 101 if (!progressPath) return; 102 103 var pathLength = progressPath.getTotalLength(); 104 progressPath.style.transition = 'none'; 105 progressPath.style.strokeDasharray = pathLength + ' ' + pathLength; 106 progressPath.style.strokeDashoffset = pathLength; 107 requestAnimationFrame(function() { 108 progressPath.style.transition = 'stroke-dashoffset 10ms linear'; 109 }); 110 111 var cachedScrollHeight = document.documentElement.scrollHeight - window.innerHeight; 112 113 function updateProgress() { 114 var scroll = window.scrollY || window.pageYOffset; 115 var progress = pathLength - (scroll * pathLength / cachedScrollHeight); 116 progressPath.style.strokeDashoffset = progress; 117 } 118 119 updateProgress(); 120 window.addEventListener('scroll', updateProgress, { passive: true }); 121 122 123 window.addEventListener('resize', function() { 124 cachedScrollHeight = document.documentElement.scrollHeight - window.innerHeight; 125 updateProgress(); 126 }, { passive: true }); 127 128 var offset = 50; 129 var progressWrap = document.querySelector('.progress-wrap'); 130 131 window.addEventListener('scroll', function () { 132 if (!progressWrap) return; 133 if ((window.scrollY || window.pageYOffset) > offset) { 134 progressWrap.classList.add('active-progress'); 135 } else { 136 progressWrap.classList.remove('active-progress'); 137 } 138 }, { passive: true }); 139 140 if (progressWrap) { 141 progressWrap.addEventListener('click', function (event) { 142 event.preventDefault(); 143 window.scrollTo({ top: 0, behavior: 'smooth' }); 144 return false;
145 }); 146 } 147}); 148 149/*-------------------------------------------------------------- 150# Media Slider 151--------------------------------------------------------------*/ 152document.addEventListener('DOMContentLoaded', function() { 153 154 // Initialize Owl Carousel for media sliders 155 const mediaSliders = document.querySelectorAll('.media-slider'); 156 157 mediaSliders.forEach(function(sliderElement) { 158 const $slider = jQuery(sliderElement); 159 160 $slider.owlCarousel({ 161 loop: true, 162 margin: 24, 163 nav: true, 164 dots: false, 165 autoplay: false, 166 autoplayHoverPause: true, 167 smartSpeed: 600, 168 navText: [ 169 '<span aria-label="Previous slide" class="prev-slide"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="19" viewBox="0 0 25 19" fill="none"><path d="M23.25 7.95508C23.9404 7.95508 24.5 8.51472 24.5 9.20508C24.5 9.89543 23.9404 10.4551 23.25 10.4551V9.20508V7.95508ZM0.366116 10.089C-0.122038 9.60081 -0.122038 8.80935 0.366116 8.32119L8.32107 0.366243C8.80922 -0.121912 9.60068 -0.121912 10.0888 0.366243C10.577 0.854399 10.577 1.64585 10.0888 2.13401L3.01777 9.20508L10.0888 16.2761C10.577 16.7643 10.577 17.5558 10.0888 18.0439C9.60068 18.5321 8.80922 18.5321 8.32107 18.0439L0.366116 10.089ZM23.25 9.20508V10.4551L1.25 10.4551V9.20508V7.95508L23.25 7.95508V9.20508Z" fill="#00191B" fill-opacity="0.3"/></svg></span>', 170 '<span aria-label="Next slide" class="next-slide"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="19" viewBox="0 0 25 19" fill="none"><path d="M1.25 7.95508C0.559644 7.95508 0 8.51472 0 9.20508C0 9.89543 0.559644 10.4551 1.25 10.4551L1.25 9.20508L1.25 7.95508ZM24.1339 10.089C24.622 9.60081 24.622 8.80935 24.1339 8.32119L16.1789 0.366243C15.6908 -0.121912 14.8993 -0.121912 14.4112 0.366243C13.923 0.854399 13.923 1.64585 14.4112 2.13401L21.4822 9.20508L14.4112 16.2761C13.923 16.7643 13.923 17.5558 14.4112 18.0439C14.8993 18.5321 15.6908 18.5321 16.1789 18.0439L24.1339 10.089ZM1.25 9.20508L1.25 10.4551L23.25 10.4551V9.20508V7.95508L1.25 7.95508L1.25 9.20508Z" fill="#00191B"/></svg></span>' 171 ], 172 responsive: { 173 0: { 174 items: 2, 175 center: true, 176 stagePadding: 40, 177 margin: 15, 178 nav: true, 179 loop: true, 180 }, 181 576: { 182 items: 2, 183 center: true, 184 stagePadding: 30, 185 margin: 20, 186 nav: true, 187 loop: true, 188 }, 189 768: { 190 items: 3, 191 center: false, 192 stagePadding: 0, 193 margin: 30, 194 nav: true, 195 loop: true, 196 }, 197 992: { 198 items: 4, 199 center: false, 200 stagePadding: 0, 201 margin: 40, 202 nav: true, 203 loop: true, 204 }, 205 1200: { 206 items: 5, 207 center: false, 208 stagePadding: 0, 209 margin: 50, 210 nav: true, 211 loop: true, 212 } 213 }, 214 onInitialized: function(event) { 215 addAccessibilityLabels(sliderElement, event); 216 }, 217 onChanged: function(event) { 218 addAccessibilityLabels(sliderElement, event); 219 } 220 }); 221 222 function addAccessibilityLabels(sliderElement, event) { 223 const navButtons = sliderElement.querySelectorAll('.owl-nav button'); 224 navButtons.forEach(function(button, index) { 225 if (index === 0) { 226 button.setAttribute('aria-label', 'Previous slide'); 227 } else { 228 button.setAttribute('aria-label', 'Next slide'); 229 } 230 }); 231 232 const dotButtons = sliderElement.querySelectorAll('.owl-dots button'); 233 dotButtons.forEach(function(button, index) { 234 const slideNumber = index + 1; 235 button.setAttribute('aria-label', 'Go to slide ' + slideNumber); 236 237 if (button.classList.contains('active')) { 238 button.setAttribute('aria-label', 'Go to slide ' + slideNumber + ' (current)'); 239 button.setAttribute('aria-current', 'true'); 240 } else { 241 button.removeAttribute('aria-current'); 242 } 243 }); 244 } 245 246 const sliderLinks = sliderElement.querySelectorAll('.slider-image-link'); 247 248 sliderLinks.forEach(function(link) { 249 link.addEventListener('mouseenter', function() { 250 $slider.trigger('stop.owl.autoplay'); 251 }); 252 253 link.addEventListener('mouseleave', function() { 254 $slider.trigger('play.owl.autoplay'); 255 }); 256 }); 257 }); 258 259 const plusIcons = document.querySelectorAll('.slider-plus-icon'); 260 261 plusIcons.forEach(function(icon) { 262 icon.addEventListener('keydown', function(e) { 263 if (e.key === 'Enter' || e.key === ' ') { 264 e.preventDefault(); 265 window.location.href = icon.getAttribute('href'); 266 } 267 }); 268 }); 269}); 270 271/*-------------------------------------------------------------- 272# Mansory Slider 273--------------------------------------------------------------*/ 274document.addEventListener('DOMContentLoaded', function() { 275 276 const masonrySliders = document.querySelectorAll('.masonry-slider'); 277
278 masonrySliders.forEach(function(sliderElement) { 279 const $slider = jQuery(sliderElement); 280 281 $slider.owlCarousel({ 282 loop: true, 283 margin: 24, 284 nav: true, 285 dots: true, 286 autoplay: false, 287 autoplayHoverPause: true, 288 smartSpeed: 600, 289 navText: [ 290 '<span aria-label="Previous slide">â¹</span>', 291 '<span aria-label="Next slide">âº</span>' 292 ], 293 responsive: { 294 0: { 295 items: 1, 296 center: true, 297 stagePadding: 40, 298 margin: 15, 299 nav: true, 300 loop: true, 301 dots: false 302 }, 303 576: { 304 items: 2, 305 center: true, 306 stagePadding: 30, 307 margin: 20, 308 nav: true, 309 loop: true, 310 dots: false 311 }, 312 768: { 313 items: 3, 314 center: true, 315 stagePadding: 0, 316 margin: 20, 317 nav: true, 318 loop: true, 319 dots: false 320 }, 321 992: { 322 items: 4, 323 center: true, 324 stagePadding: 0, 325 margin: 24, 326 nav: true, 327 loop: true, 328 }, 329 1200: { 330 items: 5, 331 center: false, 332 stagePadding: 0, 333 margin: 24, 334 nav: true, 335 loop: true, 336 } 337 }, 338 onInitialized: function() { 339 fixARIAIssues(sliderElement); 340 }, 341 onTranslated: function() { 342 fixARIAIssues(sliderElement); 343 }, 344 onRefreshed: function() { 345 fixARIAIssues(sliderElement); 346 }, 347 onChanged: function(event) { 348 const dots = sliderElement.querySelectorAll('.owl-dot'); 349 dots.forEach(function(dot, index) { 350 if (dot.classList.contains('active')) { 351 dot.setAttribute('aria-label', 'Go to slide ' + (index + 1) + ' (current)'); 352 dot.setAttribute('aria-current', 'true'); 353 } else { 354 dot.setAttribute('aria-label', 'Go to slide ' + (index + 1)); 355 dot.removeAttribute('aria-current'); 356 } 357 }); 358 } 359 }); 360 361 const sliderLinks = sliderElement.querySelectorAll('.slider-image-link, .slider-image-wrapper'); 362 363 sliderLinks.forEach(function(element) { 364 element.addEventListener('mouseenter', function() { 365 $slider.trigger('stop.owl.autoplay'); 366 }); 367 368 element.addEventListener('mouseleave', function() { 369 $slider.trigger('play.owl.autoplay'); 370 }); 371 }); 372 373 const navButtons = sliderElement.querySelectorAll('.owl-nav button'); 374 navButtons.forEach(function(button) { 375 button.addEventListener('keydown', function(e) { 376 if (e.key === 'Enter' || e.key === ' ') { 377 e.preventDefault(); 378 button.click(); 379 } 380 }); 381 }); 382 383 const dotButtons = sliderElement.querySelectorAll('.owl-dot'); 384 dotButtons.forEach(function(button) { 385 button.addEventListener('keydown', function(e) { 386 if (e.key === 'Enter' || e.key === ' ') { 387 e.preventDefault(); 388 button.click(); 389 } 390 }); 391 }); 392 }); 393 394 function fixARIAIssues(sliderElement) { 395 const $slider = jQuery(sliderElement); 396
397 $slider.removeAttr('role aria-live aria-atomic'); 398 $slider.attr({ 399 'role': 'region', 400 'aria-label': 'Gallery carousel' 401 }); 402 403 const navButtons = sliderElement.querySelectorAll('.owl-nav button'); 404 navButtons.forEach(function(button) { 405 button.removeAttribute('role'); 406 button.setAttribute('tabindex', '0'); 407 408 if (button.classList.contains('owl-prev')) { 409 button.setAttribute('aria-label', 'Previous slide'); 410 } else if (button.classList.contains('owl-next')) { 411 button.setAttribute('aria-label', 'Next slide'); 412 } 413 }); 414 415 const dotButtons = sliderElement.querySelectorAll('.owl-dots button'); 416 dotButtons.forEach(function(button, index) { 417 button.removeAttribute('role'); 418 button.setAttribute('tabindex', '0'); 419 button.setAttribute('aria-label', 'Go to slide ' + (index + 1)); 420 }); 421 } 422}); 423 424/*-------------------------------------------------------------- 425# Logos Slider 426--------------------------------------------------------------*/ 427(function() { 428 'use strict'; 429 430 function initLogosSlider() { 431 const logosSliders = document.querySelectorAll('.logos-slider'); 432 433 if (logosSliders.length === 0) return; 434 435 logosSliders.forEach(function(sliderElement) { 436 const $slider = jQuery(sliderElement); 437 438 if ($slider.hasClass('owl-loaded')) { 439 return; 440 } 441 442 const isMobile = window.innerWidth < 768; 443 444 $slider.owlCarousel({ 445 slideTransition: 'linear', 446 autoplaySpeed: 6000, 447 autoWidth: !isMobile, 448 center: false, 449 loop: true, 450 autoplay: !isMobile, 451 autoplayTimeout: 6000, 452 autoplayHoverPause: true, 453 dots: false, 454 smartSpeed: isMobile ? 800 : 6000, 455 mouseDrag: true, 456 touchDrag: true, 457 nav: isMobile, 458 slideBy: 1, 459 navText: [ 460 '<span aria-label="Previous slide">â¹</span>', 461 '<span aria-label="Next slide">âº</span>' 462 ], 463 responsiveClass: true, 464 responsive: { 465 0: { 466 items: 2, 467 margin: 10, 468 autoWidth: false, 469 autoplay: false, 470 nav: true, 471 slideTransition: 'linear', 472 smartSpeed: 800, 473 slideBy: 1, 474 }, 475 545: { 476 items: 3, 477 margin: 30, 478 autoWidth: false, 479 autoplay: false, 480 nav: true, 481 slideTransition: 'linear', 482 smartSpeed: 800, 483 slideBy: 1, 484 }, 485 768: { 486 items: 4, 487 margin: 40, 488 autoWidth: true, 489 autoplay: true, 490 nav: false, 491 slideTransition: 'linear', 492 smartSpeed: 6000, 493 }, 494 992: { 495 items: 5, 496 margin: 50, 497 autoWidth: true, 498 autoplay: true, 499 nav: false, 500 slideTransition: 'linear', 501 smartSpeed: 6000, 502 }, 503 1200: { 504 items: 6, 505 margin: 60, 506 autoWidth: true, 507 autoplay: true, 508 nav: false, 509 slideTransition: 'linear', 510 smartSpeed: 6000, 511 } 512 }, 513 onInitialized: function(event) { 514 fixClonedLinks($slider); 515 fixARIAIssues(sliderElement); 516 }, 517 onTranslated: function(event) { 518 fixClonedLinks($slider); 519 fixARIAIssues(sliderElement); 520 }, 521 onRefreshed: function(event) { 522 fixARIAIssues(sliderElement); 523 } 524 }); 525 526 let isDragging = false;
527 528 $slider.on('drag.owl.carousel', function(event) { 529 isDragging = true; 530 }); 531 532 $slider.on('dragged.owl.carousel', function(event) { 533 setTimeout(function() { 534 isDragging = false; 535 }, 100); 536 }); 537 538 $slider.on('click', '.logo-link', function(e) { 539 if (isDragging) { 540 e.preventDefault(); 541 return false; 542 } 543 }); 544 }); 545 } 546 547 function fixARIAIssues(sliderElement) { 548 const $slider = jQuery(sliderElement); 549 550 $slider.removeAttr('role aria-live aria-atomic'); 551 $slider.attr({ 552 'role': 'region', 553 'aria-label': 'Logo carousel' 554 }); 555 556 const navButtons = sliderElement.querySelectorAll('.owl-nav button'); 557 navButtons.forEach(function(button) { 558 button.removeAttribute('role'); 559 560 if (button.classList.contains('owl-prev')) { 561 button.setAttribute('aria-label', 'Previous slide'); 562 } else if (button.classList.contains('owl-next')) { 563 button.setAttribute('aria-label', 'Next slide'); 564 } 565 }); 566 } 567 568 function fixClonedLinks($slider) { 569 const allItems = $slider.find('.owl-item'); 570 571 allItems.each(function() { 572 const $item = jQuery(this); 573 const $link = $item.find('.logo-link'); 574 575 if ($item.hasClass('cloned') && $link.length > 0) { 576 const href = $link.attr('href'); 577 const title = $link.attr('title'); 578 const target = $link.attr('target'); 579 const rel = $link.attr('rel'); 580 const $img = $link.find('img').clone(); 581 582 $link.replaceWith( 583 jQuery('<a></a>') 584 .attr('href', href) 585 .attr('title', title) 586 .attr('target', target) 587 .attr('rel', rel) 588 .attr('aria-label', title) 589 .addClass('logo-link') 590 .append($img) 591 ); 592 } 593 }); 594 } 595 596 if (document.readyState === 'loading') { 597 document.addEventListener('DOMContentLoaded', initLogosSlider); 598 } else { 599 initLogosSlider(); 600 } 601 602 let resizeTimer; 603 window.addEventListener('resize', function() { 604 clearTimeout(resizeTimer); 605 resizeTimer = setTimeout(function() { 606 const sliders = document.querySelectorAll('.logos-slider.owl-loaded'); 607 sliders.forEach(function(slider) { 608 jQuery(slider).trigger('destroy.owl.carousel'); 609 jQuery(slider).removeClass('owl-loaded owl-drag'); 610 }); 611 initLogosSlider(); 612 }, 250); 613 }); 614 615})(); 616 617/*-------------------------------------------------------------- 618# FAQs Accordion 619--------------------------------------------------------------*/ 620document.addEventListener('DOMContentLoaded', function() { 621 622 const faqSections = document.querySelectorAll('.faq-accordion'); 623 624 faqSections.forEach(function(accordion) { 625 const faqButtons = accordion.querySelectorAll('.faq-question'); 626 627 faqButtons.forEach(function(button) { 628 button.addEventListener('click', function() { 629 const answer = this.nextElementSibling; 630 const isOpen = this.getAttribute('aria-expanded') === 'true'; 631 632 faqButtons.forEach(function(otherButton) { 633 const otherAnswer = otherButton.nextElementSibling; 634 otherButton.setAttribute('aria-expanded', 'false'); 635 otherAnswer.setAttribute('aria-hidden', 'true'); 636 otherAnswer.setAttribute('hidden', ''); 637 }); 638 639 if (!isOpen) { 640 this.setAttribute('aria-expanded', 'true'); 641 answer.setAttribute('aria-hidden', 'false'); 642 answer.removeAttribute('hidden'); 643 } 644 }); 645 }); 646 }); 647}); 648 649/*-------------------------------------------------------------- 650# Testimonials Slider 651--------------------------------------------------------------*/ 652document.addEventListener('DOMContentLoaded', function() { 653 654 const testimonialsSliders = document.querySelectorAll('.testimonials-slider'); 655
656 testimonialsSliders.forEach(function(sliderElement) { 657 const $slider = jQuery(sliderElement); 658 659 $slider.owlCarousel({ 660 loop: true, 661 margin: 30, 662 nav: true, 663 dots: true, 664 autoplay: false, 665 autoplayTimeout: 6000, 666 autoplayHoverPause: true, 667 smartSpeed: 600, 668 items: 1, 669 navText: [ 670 '<span aria-label="Previous">â¹</span>', 671 '<span aria-label="Next">âº</span>' 672 ], 673 responsive: { 674 0: { 675 nav: false, 676 margin: 15 677 }, 678 768: { 679 nav: true, 680 margin: 30 681 } 682 }, 683 onInitialized: function() { 684 const navButtons = sliderElement.querySelectorAll('.owl-nav button'); 685 const dotButtons = sliderElement.querySelectorAll('.owl-dots button'); 686 687 navButtons.forEach(function(button) { 688 button.setAttribute('role', 'button'); 689 button.setAttribute('tabindex', '0'); 690 }); 691 692 dotButtons.forEach(function(button, index) { 693 button.setAttribute('role', 'button'); 694 button.setAttribute('tabindex', '0'); 695 button.setAttribute('aria-label', 'Go to testimonial ' + (index + 1)); 696 }); 697 }, 698 onChanged: function(event) { 699 const dots = sliderElement.querySelectorAll('.owl-dot'); 700 dots.forEach(function(dot, index) { 701 if (dot.classList.contains('active')) { 702 dot.setAttribute('aria-label', 'Current testimonial ' + (index + 1)); 703 } else { 704 dot.setAttribute('aria-label', 'Go to testimonial ' + (index + 1)); 705 } 706 }); 707 } 708 }); 709 }); 710}); 711 712/*-------------------------------------------------------------- 713# Tabs Accordion 714--------------------------------------------------------------*/ 715document.addEventListener('DOMContentLoaded', function() { 716 initTabsAccordion(); 717}); 718 719function initTabsAccordion() { 720 const sections = document.querySelectorAll('.tabs-accordion-section'); 721 722 sections.forEach(section => { 723 initDesktopTabs(section); 724 initMobileAccordion(section); 725 }); 726} 727 728function initDesktopTabs(section) { 729 const tabButtons = section.querySelectorAll('.tab-button'); 730 const tabPanels = section.querySelectorAll('.tab-panel'); 731 732 tabButtons.forEach(button => { 733 button.addEventListener('click', () => { 734 const tabId = button.getAttribute('data-tab'); 735 const tabIndex = tabId.split('-')[1]; 736 const targetPanel = section.querySelector('#tabpanel-' + tabIndex); 737 738 if (button.classList.contains('active')) { 739 return; 740 } 741 742 tabButtons.forEach(btn => { 743 btn.classList.remove('active'); 744 btn.setAttribute('aria-selected', 'false'); 745 }); 746 747 tabPanels.forEach(panel => { 748 panel.classList.remove('active'); 749 panel.setAttribute('hidden', 'hidden'); 750 }); 751 752 button.classList.add('active'); 753 button.setAttribute('aria-selected', 'true'); 754 targetPanel.classList.add('active'); 755 targetPanel.removeAttribute('hidden'); 756 757 announceToScreenReader('Tab ' + button.querySelector('.tab-title').textContent + ' selected'); 758 }); 759 760 button.addEventListener('keydown', (e) => { 761 const buttonsArray = Array.from(tabButtons); 762 const currentIndex = buttonsArray.indexOf(button); 763 let nextIndex; 764 765 switch(e.key) { 766 case 'ArrowDown': 767 case 'ArrowRight': 768 e.preventDefault(); 769 nextIndex = (currentIndex + 1) % buttonsArray.length; 770 buttonsArray[nextIndex].focus(); 771 buttonsArray[nextIndex].click(); 772 break; 773 case 'ArrowUp': 774 case 'ArrowLeft': 775 e.preventDefault(); 776 nextIndex = (currentIndex - 1 + buttonsArray.length) % buttonsArray.length; 777 buttonsArray[nextIndex].focus(); 778 buttonsArray[nextIndex].click(); 779 break; 780 case 'Home': 781 e.preventDefault(); 782 buttonsArray[0].focus(); 783 buttonsArray[0].click(); 784 break; 785 case 'End': 786 e.preventDefault(); 787 buttonsArray[buttonsArray.length - 1].focus(); 788 buttonsArray[buttonsArray.length - 1].click(); 789 break; 790 case 'Enter': 791 case ' ': 792 e.preventDefault(); 793 button.click(); 794 break; 795 } 796 }); 797 }); 798} 799 800function initMobileAccordion(section) { 801 const accordionButtons = section.querySelectorAll('.accordion-button'); 802 const accordionContents = section.querySelectorAll('.accordion-content'); 803 804 function updateFocusableElements(content, makesFocusable) { 805 const focusableElements = content.querySelectorAll('a, button, input, select, textarea, [tabindex]:not
805([tabindex="-1"])'); 806 807 focusableElements.forEach(element => { 808 if (makesFocusable) { 809 const originalTabindex = element.getAttribute('data-original-tabindex') || '0'; 810 element.setAttribute('tabindex', originalTabindex); 811 } else { 812 if (!element.hasAttribute('data-original-tabindex')) { 813 element.setAttribute('data-original-tabindex', element.getAttribute('tabindex') || '0'); 814 } 815 element.setAttribute('tabindex', '-1'); 816 } 817 }); 818 } 819 820 accordionContents.forEach(content => { 821 if (content.getAttribute('aria-hidden') === 'true') { 822 updateFocusableElements(content, false); 823 } 824 }); 825 826 accordionButtons.forEach(button => { 827 button.addEventListener('click', () => { 828 const index = button.getAttribute('data-index'); 829 const content = section.querySelector('#accordion-content-' + index); 830 const isExpanded = button.getAttribute('aria-expanded') === 'true'; 831 832 accordionButtons.forEach(btn => { 833 btn.classList.add('collapsed'); 834 btn.setAttribute('aria-expanded', 'false'); 835 }); 836 accordionContents.forEach(cnt => { 837 cnt.classList.remove('show'); 838 cnt.setAttribute('aria-hidden', 'true'); 839 updateFocusableElements(cnt, false); 840 }); 841 842 if (!isExpanded) { 843 button.classList.remove('collapsed'); 844 button.setAttribute('aria-expanded', 'true'); 845 content.classList.add('show'); 846 content.setAttribute('aria-hidden', 'false'); 847 updateFocusableElements(content, true); 848 announceToScreenReader(button.querySelector('.accordion-title').textContent + ' expanded'); 849 } else { 850 announceToScreenReader(button.querySelector('.accordion-title').textContent + ' collapsed'); 851 } 852 }); 853 854 button.addEventListener('keydown', (e) => { 855 const buttonsArray = Array.from(accordionButtons); 856 const currentIndex = buttonsArray.indexOf(button); 857 858 switch(e.key) { 859 case 'Enter': 860 case ' ': 861 e.preventDefault(); 862 button.click(); 863 break; 864 case 'ArrowDown': 865 e.preventDefault(); 866 const nextIndex = (currentIndex + 1) % buttonsArray.length; 867 buttonsArray[nextIndex].focus(); 868 break; 869 case 'ArrowUp': 870 e.preventDefault(); 871 const prevIndex = (currentIndex - 1 + buttonsArray.length) % buttonsArray.length; 872 buttonsArray[prevIndex].focus(); 873 break; 874 case 'Home': 875 e.preventDefault(); 876 buttonsArray[0].focus(); 877 break; 878 case 'End': 879 e.preventDefault(); 880 buttonsArray[buttonsArray.length - 1].focus(); 881 break; 882 } 883 }); 884 }); 885} 886 887function announceToScreenReader(message) {
888 let liveRegion = document.getElementById('aria-live-region'); 889 890 if (!liveRegion) { 891 liveRegion = document.createElement('div'); 892 liveRegion.id = 'aria-live-region'; 893 liveRegion.setAttribute('aria-live', 'polite'); 894 liveRegion.setAttribute('aria-atomic', 'true'); 895 liveRegion.className = 'sr-only'; 896 document.body.appendChild(liveRegion); 897 } 898 899 liveRegion.textContent = message; 900 901 setTimeout(() => { 902 liveRegion.textContent = ''; 903 }, 1000); 904} 905 906/*-------------------------------------------------------------- 907# Team Member Lightbox 908--------------------------------------------------------------*/ 909const TeamLightbox = { 910 open: function(index, blockId) { 911 const lightbox = document.getElementById('team-lightbox-' + blockId); 912 const teamMembers = window.teamMembersData[blockId]; 913 914 if (!lightbox || !teamMembers || !teamMembers[index]) { 915 console.error('Team member data not found'); 916 return; 917 } 918 919 const member = teamMembers[index]; 920 921 const imageEl = document.getElementById('lightbox-image-' + blockId); 922 const titleEl = document.getElementById('lightbox-title-' + blockId); 923 const positionEl = lightbox.querySelector('.team-lightbox-position'); 924 const descriptionEl = lightbox.querySelector('.team-lightbox-description'); 925 926 if (imageEl && member.image) { 927 imageEl.src = member.image; 928 imageEl.alt = member.image_alt || member.name; 929 } 930 931 if (titleEl) { 932 titleEl.textContent = member.name; 933 } 934 935 if (positionEl) { 936 if (member.position) { 937 positionEl.textContent = member.position; 938 positionEl.style.display = 'block'; 939 } else { 940 positionEl.style.display = 'none'; 941 } 942 } 943 944 if (descriptionEl) { 945 descriptionEl.innerHTML = member.content || ''; 946 } 947 948 lightbox.style.display = 'flex'; 949 requestAnimationFrame(function() { 950 requestAnimationFrame(function() { 951 lightbox.classList.add('active'); 952 }); 953 }); 954 955 document.body.classList.add('team-lightbox-open'); 956 957 const closeBtn = lightbox.querySelector('.team-lightbox-close'); 958 if (closeBtn) { 959 closeBtn.focus(); 960 } 961 }, 962 963 close: function(blockId) { 964 const lightbox = document.getElementById('team-lightbox-' + blockId); 965 966 if (!lightbox) { 967 return; 968 } 969 970 lightbox.classList.remove('active'); 971 972 setTimeout(() => { 973 lightbox.style.display = 'none'; 974 document.body.classList.remove('team-lightbox-open'); 975 }, 300); 976 }, 977 978 init: function() { 979 document.querySelectorAll('.team-lightbox').forEach(lightbox => { 980 lightbox.addEventListener('click', function(e) { 981 if (e.target === this) { 982 const blockId = this.id.replace('team-lightbox-', ''); 983 TeamLightbox.close(blockId); 984 } 985 }); 986 }); 987 988 document.addEventListener('keydown', function(e) { 989 if (e.key === 'Escape') { 990 const activeLightbox = document.querySelector('.team-lightbox.active'); 991 if (activeLightbox) { 992 const blockId = activeLightbox.id.replace('team-lightbox-', ''); 993 TeamLightbox.close(blockId); 994 } 995 } 996 }); 997 } 998}; 999 1000if (document.readyState === 'loading') { 1001 document.addEventListener('DOMContentLoaded', function() { 1002 TeamLightbox.init(); 1003 }); 1004} else { 1005 TeamLightbox.init(); 1006} 1007 1008/*-------------------------------------------------------------- 1009# Load More Press Releases 1010--------------------------------------------------------------*/ 1011document.addEventListener('DOMContentLoaded', function () { 1012 const postCards = document.querySelectorAll('.press-release-item');
1013 const loadMoreBtn = document.querySelector('.load-more-press-release'); 1014 1015 let currentIndex = 0; 1016 const increment = 10; 1017 1018 function showPosts() { 1019 let lastShownIndex = -1; 1020 for (let i = currentIndex; i < currentIndex + increment && i < postCards.length; i++) { 1021 postCards[i].style.display = ''; 1022 lastShownIndex = i; 1023 } 1024 currentIndex += increment; 1025 if (currentIndex >= postCards.length) { 1026 if (loadMoreBtn) loadMoreBtn.style.visibility = 'hidden'; 1027 } 1028 return lastShownIndex; 1029 } 1030 1031 postCards.forEach(card => card.style.display = 'none'); 1032 showPosts(); 1033 1034 if (loadMoreBtn) { 1035 function handleLoadMore(e) { 1036 e.preventDefault(); 1037 const lastShownIndex = showPosts(); 1038 if (lastShownIndex !== -1) { 1039 const lastCard = postCards[lastShownIndex]; 1040 lastCard.scrollIntoView({ behavior: 'smooth', block: 'start' }); 1041 } 1042 } 1043 loadMoreBtn.addEventListener('click', handleLoadMore); 1044 loadMoreBtn.addEventListener('touchstart', handleLoadMore); 1045 } 1046}); 1047 1048/*-------------------------------------------------------------- 1049# Load More Reviews 1050--------------------------------------------------------------*/ 1051document.addEventListener('DOMContentLoaded', function () { 1052 const postCards = document.querySelectorAll('.review-item'); 1053 const loadMoreBtn = document.querySelector('.load-more-reviews'); 1054 1055 let currentIndex = 0; 1056 const increment = 10; 1057 1058 function showPosts() { 1059 let lastShownIndex = -1; 1060 for (let i = currentIndex; i < currentIndex + increment && i < postCards.length; i++) { 1061 postCards[i].style.display = ''; 1062 lastShownIndex = i; 1063 } 1064 currentIndex += increment; 1065 if (currentIndex >= postCards.length) { 1066 if (loadMoreBtn) loadMoreBtn.style.visibility = 'hidden'; 1067 } 1068 return lastShownIndex; 1069 } 1070 1071 postCards.forEach(card => card.style.display = 'none'); 1072 showPosts(); 1073 1074 if (loadMoreBtn) { 1075 function handleLoadMore(e) { 1076 e.preventDefault(); 1077 const lastShownIndex = showPosts(); 1078 if (lastShownIndex !== -1) { 1079 const lastCard = postCards[lastShownIndex]; 1080 lastCard.scrollIntoView({ behavior: 'smooth', block: 'start' }); 1081 } 1082 } 1083 loadMoreBtn.addEventListener('click', handleLoadMore); 1084 loadMoreBtn.addEventListener('touchstart', handleLoadMore); 1085 } 1086}); 1087 1088/*-------------------------------------------------------------- 1089# Reviews Read More Block 1090--------------------------------------------------------------*/ 1091(function() { 1092 'use strict'; 1093 1094 let initializedButtons = new Set(); 1095 1096 function initReviewsAccordion(reinit = false) { 1097 if (!reinit) { 1098 initializedButtons.clear(); 1099 } 1100 1101 const readMoreButtons = document.querySelectorAll('.review-read-more'); 1102 1103 if (readMoreButtons.length === 0) { 1104 return; 1105 } 1106 1107 readMoreButtons.forEach((button, index) => { 1108 const reviewId = button.getAttribute('data-review-id'); 1109 1110 const parentCard = button.closest('.review-card'); 1111 const isHidden = parentCard && ( 1112 parentCard.classList.contains('hidden') || 1113 parentCard.style.display === 'none' || 1114 window.getComputedStyle(parentCard).display === 'none' 1115 ); 1116 1117 if (isHidden) { 1118 return; 1119 } 1120 1121 if (initializedButtons.has(reviewId) && !reinit) { 1122 return; 1123 } 1124 1125 const reviewText = document.getElementById(reviewId); 1126 1127 if (!reviewText) { 1128 return; 1129 } 1130 1131 const isVisible = !parentCard || window.getComputedStyle(parentCard).display !== 'none'; 1132 1133 if (!isVisible) { 1134 return; 1135 } 1136 1137 // Batch all reads before any writes 1138 const computedStyle = window.getComputedStyle(reviewText); 1139 const maxHeight = parseFloat(computedStyle.maxHeight); 1140 const scrollHeight = reviewText.scrollHeight; 1141 1142 const needsTruncation = scrollHeight > (maxHeight + 5); 1143 1144 if (!needsTruncation) { 1145 button.style.display = 'none'; 1146 reviewText.classList.add('no-truncation'); 1147 initializedButtons.add(reviewId); 1148 return; 1149 } 1150 1151 button.reviewTextElement = reviewText; 1152 button.buttonIndex = index; 1153 1154 const newButton = button.cloneNode(true); 1155 newButton.reviewTextElement = reviewText; 1156 newButton.buttonIndex = index; 1157 button.parentNode.replaceChild(newButton, button); 1158 1159 newButton.addEventListener('click', function(e) { 1160 e.preventDefault(); 1161 e.stopPropagation(); 1162 1163 const currentState = this.getAttribute('aria-expanded'); 1164 const isExpanded = currentState === 'true'; 1165 const textElement = this.reviewTextElement; 1166 1167 if (isExpanded) { 1168 textElement.classList.remove('expanded'); 1169 this.setAttribute('aria-expanded', 'false'); 1170 this.textContent = 'Read More'; 1171 1172 const cardEl = textElement.closest('.review-card'); 1173 if (cardEl && cardEl.getBoundingClientRect().top < 0) { 1174 cardEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); 1175 } 1176 } else { 1177 textElement.classList.add('expanded'); 1178 this.setAttribute('aria-expanded', 'true'); 1179 this.textContent = 'Read Less'; 1180 } 1181 }); 1182 1183 newButton.addEventListener('keydown', function(e) { 1184 if (e.key === 'Enter' || e.key === ' ') { 1185 e.preventDefault(); 1186 this.click(); 1187 } 1188 }); 1189 1190 initializedButtons.add(reviewId); 1191 }); 1192 } 1193 1194 function initLoadMore() {
1195 const loadMoreBtn = document.querySelector('.load-more-reviews'); 1196 if (!loadMoreBtn) { 1197 return; 1198 } 1199 1200 const reviewsGrid = document.querySelector('.reviews-grid'); 1201 if (!reviewsGrid) { 1202 return; 1203 } 1204 1205 const reviewItems = reviewsGrid.querySelectorAll('.review-item'); 1206 const itemsPerPage = 6; 1207 let currentPage = 1; 1208 1209 function showItems(page) { 1210 const end = page * itemsPerPage; 1211 1212 reviewItems.forEach((item, index) => { 1213 if (index < end) { 1214 item.classList.remove('hidden'); 1215 item.style.display = 'block'; 1216 } else { 1217 item.classList.add('hidden'); 1218 item.style.display = 'none'; 1219 } 1220 }); 1221 1222 if (end >= reviewItems.length) { 1223 loadMoreBtn.style.display = 'none'; 1224 } 1225 } 1226 1227 showItems(currentPage); 1228 1229 loadMoreBtn.addEventListener('click', function(e) { 1230 e.preventDefault(); 1231 1232 currentPage++; 1233 showItems(currentPage); 1234 1235 setTimeout(() => { 1236 initReviewsAccordion(true); 1237 }, 300); 1238 1239 const firstNewItemIndex = (currentPage - 1) * itemsPerPage; 1240 const firstNewItem = reviewItems[firstNewItemIndex]; 1241 if (firstNewItem) { 1242 setTimeout(() => { 1243 firstNewItem.scrollIntoView({ 1244 behavior: 'smooth', 1245 block: 'nearest' 1246 }); 1247 }, 400); 1248 } 1249 1250 const remainingItems = reviewItems.length - (currentPage * itemsPerPage); 1251 if (remainingItems > 0 && remainingItems < itemsPerPage) { 1252 loadMoreBtn.textContent = `Load More (${remainingItems} remaining)`; 1253 } 1254 }); 1255 } 1256 1257 function init() { 1258 function start() { 1259 if (document.fonts && document.fonts.ready) { 1260 document.fonts.ready.then(() => { 1261 setTimeout(() => { 1262 initReviewsAccordion(); 1263 initLoadMore(); 1264 }, 150); 1265 }); 1266 } else { 1267 setTimeout(() => { 1268 initReviewsAccordion(); 1269 initLoadMore(); 1270 }, 300); 1271 } 1272 } 1273 1274 if (document.readyState === 'loading') { 1275 document.addEventListener('DOMContentLoaded', start); 1276 } else { 1277 start(); 1278 } 1279 } 1280 1281 init(); 1282 1283 window.ReviewsBlock = { 1284 init: init, 1285 initReviewsAccordion: initReviewsAccordion, 1286 initLoadMore: initLoadMore 1287 }; 1288 1289})(); 1290 1291/*-------------------------------------------------------------- 1292# Counter Block Animation 1293--------------------------------------------------------------*/ 1294(function() { 1295 'use strict'; 1296 1297 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 1298 1299 1300 function easeOutQuad(t, b, c, d) { 1301 t /= d; 1302 return -c * t * (t - 2) + b; 1303 } 1304 1305 function formatNumber(num) { 1306 return Math.ceil(num).toLocaleString(); 1307 } 1308 1309 function animateCounter(element, targetValue, duration) { 1310 if (prefersReducedMotion) { 1311 element.textContent = formatNumber(targetValue); 1312 element.setAttribute('aria-label', `${formatNumber(targetValue)}`); 1313 return; 1314 } 1315 1316 const startValue = 0; 1317 const startTime = performance.now(); 1318 let animationId; 1319 1320 function updateCounter(currentTime) { 1321 const elapsed = currentTime - startTime; 1322 1323 if (elapsed < duration) { 1324 const currentValue = easeOutQuad(elapsed, startValue, targetValue - startValue, duration); 1325 element.textContent = formatNumber(currentValue); 1326 animationId = requestAnimationFrame(updateCounter); 1327 } else { 1328 element.textContent = formatNumber(targetValue); 1329 element.setAttribute('aria-label', `${formatNumber(targetValue)}`); 1330 element.setAttribute('aria-live', 'off'); 1331 } 1332 } 1333 1334 animationId = requestAnimationFrame(updateCounter); 1335 element.animationId = animationId; 1336 } 1337 1338 function startCounter(counter) { 1339 const targetCount = parseInt(counter.getAttribute('data-count'), 10); 1340 const duration = parseInt(counter.getAttribute('data-duration'), 10) || 3000; 1341 if (isNaN(targetCount)) return; 1342 counter.classList.add('animated'); 1343 counter.setAttribute('aria-label', 'Counting to ' + formatNumber(targetCount)); 1344 animateCounter(counter, targetCount, duration); 1345 } 1346
1347 function init() { 1348 const counterSections = document.querySelectorAll('.counter-section'); 1349 if (counterSections.length === 0) return; 1350 1351 const observer = new IntersectionObserver(function(entries) { 1352 entries.forEach(function(entry) { 1353 if (entry.isIntersecting) { 1354 entry.target.querySelectorAll('.counter-number:not(.animated)') 1355 .forEach(startCounter); 1356 observer.unobserve(entry.target); 1357 } 1358 }); 1359 }, { root: null, rootMargin: '0px', threshold: 0.1 }); 1360 1361 counterSections.forEach(function(section) { observer.observe(section); }); 1362 } 1363 1364 if (document.readyState === 'loading') { 1365 document.addEventListener('DOMContentLoaded', init); 1366 } else { 1367 init(); 1368 } 1369 1370})(); 1371 1372/*-------------------------------------------------------------- 1373# Post Load More â DESHABILITADO: reemplazado por AJAX en js/ajax.js 1374--------------------------------------------------------------*/ 1375/* 1376document.addEventListener('DOMContentLoaded', function() {
1377 const loadMoreBtn = document.querySelector('.load-more-posts'); 1378 1379 if (loadMoreBtn) { 1380 loadMoreBtn.addEventListener('click', function() { 1381 const hiddenPosts = document.querySelectorAll('.post-item.hidden'); 1382 const totalPosts = parseInt(this.getAttribute('data-total-posts')); 1383 const currentPosts = parseInt(this.getAttribute('data-current-posts')); 1384 const postsToShow = 6; 1385 let count = 0; 1386 1387 hiddenPosts.forEach(function(post) { 1388 if (count < postsToShow) { 1389 post.classList.remove('hidden'); 1390 post.style.animation = 'fadeInUp 0.5s ease'; 1391 count++; 1392 } 1393 }); 1394 1395 const newCurrentPosts = currentPosts + count; 1396 this.setAttribute('data-current-posts', newCurrentPosts); 1397 1398 if (newCurrentPosts >= totalPosts) { 1399 this.style.display = 'none'; 1400 } 1401 1402 const remainingHidden = document.querySelectorAll('.post-item.hidden'); 1403 if (remainingHidden.length === 0) { 1404 this.style.display = 'none'; 1405 } 1406 }); 1407 } 1408}); 1409*/ 1410 1411/*-------------------------------------------------------------- 1412# Post Sticky Sidebar 1413--------------------------------------------------------------*/ 1414document.addEventListener('DOMContentLoaded', function() { 1415 if (window.innerWidth < 992) return; 1416 1417 const sidebar = document.querySelector('.sidebar-sticky'); 1418 if (!sidebar) return; 1419 1420 const sidebarWrapper = document.querySelector('.sidebar-sticky-wrapper'); 1421 const breadcrumbSection = document.querySelector('.section-breadcrumbs'); 1422 const postContentSection = document.querySelector('.post-content-section'); 1423 1424 if (!sidebarWrapper || !breadcrumbSection || !postContentSection) return; 1425 1426 const stickyOffset = 100; 1427 let breadcrumbBottom, contentBottom, sidebarHeight, wrapperWidth; 1428 1429 function cacheLayout() { 1430 breadcrumbBottom = breadcrumbSection.offsetTop + breadcrumbSection.offsetHeight; 1431 contentBottom = postContentSection.offsetTop + postContentSection.offsetHeight; 1432 sidebarHeight = sidebar.offsetHeight; 1433 wrapperWidth = sidebarWrapper.offsetWidth; 1434 } 1435 1436 function handleScroll() { 1437 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 1438 const stopPoint = contentBottom - sidebarHeight - stickyOffset; 1439 1440 if (scrollTop > breadcrumbBottom) { 1441 if (scrollTop + stickyOffset > stopPoint) { 1442 sidebar.classList.remove('is-sticky'); 1443 sidebar.classList.add('is-bottom'); 1444 sidebar.style.cssText = 'position:absolute;top:auto;bottom:0;width:' + wrapperWidth + 'px'; 1445 } else { 1446 sidebar.classList.add('is-sticky'); 1447 sidebar.classList.remove('is-bottom'); 1448 sidebar.style.cssText = 'position:fixed;top:' + stickyOffset + 'px;bottom:auto;width:' + wrapperWidth + 'px'; 1449 } 1450 } else { 1451 sidebar.classList.remove('is-sticky', 'is-bottom'); 1452 sidebar.style.cssText = 'position:static;width:auto'; 1453 } 1454 } 1455 requestAnimationFrame(function() { 1456 cacheLayout(); 1457 handleScroll(); 1458 window.addEventListener('scroll', handleScroll, { passive: true }); 1459 }); 1460 1461 let resizeTimer; 1462 window.addEventListener('resize', function() { 1463 clearTimeout(resizeTimer); 1464 resizeTimer = setTimeout(function() { 1465 if (window.innerWidth >= 992) { 1466 cacheLayout(); 1467 handleScroll(); 1468 } else { 1469 sidebar.classList.remove('is-sticky', 'is-bottom'); 1470 sidebar.style.cssText = 'position:static;width:auto'; 1471 } 1472 }, 250); 1473 }); 1474}); 1475 1476/*-------------------------------------------------------------- 1477# Compare Table Slider 1478--------------------------------------------------------------*/ 1479(function () { 1480 function initCompareTable() { 1481 if (typeof jQuery === 'undefined' || typeof jQuery.fn.owlCarousel === 'undefined') { 1482 setTimeout(initCompareTable, 100); 1483 return; 1484 } 1485 1486 var $ = jQuery; 1487 1488 var $carousel = $('#comparison-table-slider'); 1489 if ($carousel.length) { 1490 $carousel.owlCarousel({ 1491 items: 1, 1492 loop: false, 1493 nav: true, 1494 dots: true, 1495 navText: [ 1496 '<span aria-label="Previous" role="button"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 640 640" fill="currentColor" aria-hidden="true"><path d="M41.4 297.4C28.9 309.9 28.9 330.1 41.4 342.6L169.4 470.6C181.9 483.2 202.2 483.2 214.7 470.6C227.2 458.1 227.2 437.8 214.7 425.4L141.3 352L576 352C593.7 352 608 337.7 608 320C608 302.3 593.7 288 576 288L141.3 288L214.6 214.6C227.1 202.1 227.1 181.8 214.6 169.3C202.1 156.8 181.8 156.8 169.3 169.3L41.4 297.4z"/></svg></span>', 1497 '<span aria-label="Next" role="button"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 640 640" fill="currentColor" aria-hidden="true"><path d="M598.6 342.6C611.1 330.1 611.1 309.8 598.6 297.3L470.6 169.3C458.1 156.8 437.8 156.8 425.3 169.3C412.8 181.8 412.8 202.1 425.3 214.6L498.7 288L64 288C46.3 288 32 302.3 32 320C32 337.7 46.3 352 64 352L498.7 352L425.3 425.4C412.8 437.9 412.8 458.2 425.3 470.7C437.8 483.2 458.1 483.2 470.6 470.7L598.6 342.7z"/></svg></span>' 1498 ], 1499 margin: 0, 1500 stagePadding: 30, 1501 autoHeight: false, 1502 autoplay: false, 1503 smartSpeed: 400, 1504 navSpeed: 400, 1505 dotsSpeed: 400, 1506 responsive: { 1507 0: { items: 1, stagePadding: 30 }, 1508 480: { items: 1, stagePadding: 60 } 1509 }, 1510 onInitialized: function (event) { 1511 $carousel.find('.owl-prev, .owl-next').attr('tabindex', '0'); 1512 }, 1513 onChanged: function (event) { 1514 var total = event.item.count; 1515 var current = event.item.index + 1; 1516 $carousel.attr('aria-label', 'Comparison, item ' + current + ' of ' + total); 1517 } 1518 }); 1519
1520 $carousel.on('keydown', '.owl-prev, .owl-next', function (e) { 1521 if (e.key === 'Enter' || e.key === ' ') { 1522 e.preventDefault(); 1523 $(this).trigger('click'); 1524 } 1525 }); 1526 } 1527 1528 $(document).on('click keydown', '.ct-tooltip-trigger', function (e) { 1529 if (e.type === 'click' || e.key === 'Enter' || e.key === ' ') { 1530 e.preventDefault(); 1531 var $btn = $(this); 1532 var $tip = $btn.next('.ct-tooltip-content'); 1533 var isOpen = $tip.hasClass('is-visible'); 1534 1535 $('.ct-tooltip-content').removeClass('is-visible').attr('aria-hidden', 'true'); 1536 $('.ct-tooltip-trigger').attr('aria-expanded', 'false'); 1537 1538 if (!isOpen) { 1539 $tip.addClass('is-visible').attr('aria-hidden', 'false'); 1540 $btn.attr('aria-expanded', 'true'); 1541 } 1542 } 1543 }); 1544 1545 $(document).on('click', function (e) { 1546 if (!$(e.target).closest('.ct-row-label').length) { 1547 $('.ct-tooltip-content').removeClass('is-visible').attr('aria-hidden', 'true'); 1548 $('.ct-tooltip-trigger').attr('aria-expanded', 'false'); 1549 } 1550 }); 1551 1552 function syncMobileLabelHeights() { 1553 var labelsEl = document.querySelector('.ct-mobile-labels'); 1554 var firstCol = $carousel[0] && $carousel[0].querySelector('.ct-mobile-col'); 1555 if (!labelsEl || !firstCol) return; 1556 1557 1558 var headerEl = firstCol.querySelector('.ct-mobile-col-header'); 1559 var headerH = headerEl ? headerEl.getBoundingClientRect().height : 0; 1560 1561 var cellEls = firstCol.querySelectorAll('.ct-mobile-cell'); 1562 var cellHeights = []; 1563 cellEls.forEach(function (el) { 1564 cellHeights.push(el.getBoundingClientRect().height); 1565 }); 1566 1567 var ctaEl = firstCol.querySelector('.ct-mobile-cta'); 1568 var ctaH = ctaEl ? ctaEl.getBoundingClientRect().height : 0; 1569 1570 // --- All writes after --- 1571 var spacer = labelsEl.querySelector('.ct-mobile-label-logo-spacer'); 1572 if (spacer) spacer.style.height = headerH + 'px'; 1573 1574 var labelRows = labelsEl.querySelectorAll('.ct-mobile-label-row'); 1575 labelRows.forEach(function (row, i) { 1576 if (cellHeights[i] !== undefined) { 1577 row.style.height = cellHeights[i] + 'px'; 1578 } 1579 }); 1580 1581 var ctaSpacer = labelsEl.querySelector('.ct-mobile-label-cta-spacer'); 1582 if (ctaSpacer) ctaSpacer.style.height = ctaH + 'px'; 1583 } 1584 1585 1586 var syncResizeTimer; 1587 function debouncedSync() { 1588 clearTimeout(syncResizeTimer); 1589 syncResizeTimer = setTimeout(syncMobileLabelHeights, 150); 1590 } 1591 1592 1593 $carousel.on('initialized.owl.carousel resized.owl.carousel', function () { 1594 requestAnimationFrame(syncMobileLabelHeights); 1595 }); 1596 $(window).on('resize', debouncedSync); 1597 setTimeout(syncMobileLabelHeights, 300); 1598 } 1599 1600 if (document.readyState === 'loading') { 1601 document.addEventListener('DOMContentLoaded', initCompareTable); 1602 } else { 1603 initCompareTable(); 1604 } 1605})(); 1606 1607/*-------------------------------------------------------------- 1608# Sticky Pricing Table 1609--------------------------------------------------------------*/ 1610(function () { 1611 var footer = document.getElementById('footer-pricing-table'); 1612 var table = document.getElementById('pricing-table'); 1613 1614 if (!footer || !table) return; 1615 1616 1617 footer.classList.remove('is-sticky'); 1618 1619 var observer = new IntersectionObserver( 1620 function (entries) { 1621 entries.forEach(function (entry) { 1622 1623 if (!entry.isIntersecting) { 1624 footer.classList.add('is-visible'); 1625 footer.removeAttribute('aria-hidden'); 1626 } else { 1627 footer.classList.remove('is-visible'); 1628 footer.setAttribute('aria-hidden', 'true'); 1629 } 1630 }); 1631 }, 1632 { 1633 1634 rootMargin: '0px 0px 0px 0px', 1635 threshold: 0 1636 } 1637 ); 1638 1639 observer.observe(table); 1640 1641 // Initial state: hidden 1642 footer.setAttribute('aria-hidden', 'true'); 1643})(); 1644/*-------------------------------------------------------------- 1645# Table of Contents â scrollspy + smooth scroll 1646--------------------------------------------------------------*/ 1647(function () { 1648 const toc = document.querySelector('.custom-toc'); 1649 if (!toc) return; 1650 1651 const links = Array.prototype.slice.call(toc.querySelectorAll('a[href^="#"]')); 1652 if (!links.length) return; 1653 1654 // Map each TOC link to its target heading 1655 const items = links 1656 .map(function (link) { 1657 const id = decodeURIComponent(link.getAttribute('href').slice(1)); 1658 const target = id ? document.getElementById(id) : null; 1659 return target ? { link: link, target: target } : null; 1660 }) 1661 .filter(Boolean); 1662 1663 if (!items.length) return; 1664 1665 // Offset for a sticky/fixed header (if any) 1666 function getOffset() { 1667 const header = document.querySelector('.site-header, header'); 1668 if (header) { 1669 const pos = window.getComputedStyle(header).position; 1670 if (pos === 'fixed' || pos === 'sticky') { 1671 return header.offsetHeight + 20; 1672 } 1673 } 1674 return 20; 1675 } 1676 1677 // Smooth scroll on click
1678 items.forEach(function (item) { 1679 item.link.addEventListener('click', function (e) { 1680 e.preventDefault(); 1681 const top = item.target.getBoundingClientRect().top + window.pageYOffset - getOffset(); 1682 window.scrollTo({ top: top, behavior: 'smooth' }); 1683 history.replaceState(null, '', item.link.getAttribute('href')); 1684 }); 1685 }); 1686 1687 // Scrollspy: mark the current section active 1688 function setActive(target) { 1689 items.forEach(function (item) { 1690 item.link.classList.toggle('toc-active', item.target === target); 1691 }); 1692 } 1693 1694 function onScroll() { 1695 // Activation line: a heading becomes active once it reaches ~70% up the viewport, 1696 // so the section highlights as soon as its heading is well into view (not only at the very top). 1697 const line = window.innerHeight * 0.7; 1698 let current = items[0].target; 1699 for (let i = 0; i < items.length; i++) { 1700 if (items[i].target.getBoundingClientRect().top <= line) { 1701 current = items[i].target; 1702 } else { 1703 break; 1704 } 1705 } 1706 setActive(current); 1707 } 1708 1709 let ticking = false; 1710 window.addEventListener('scroll', function () { 1711 if (!ticking) { 1712 window.requestAnimationFrame(function () { 1713 onScroll(); 1714 ticking = false; 1715 }); 1716 ticking = true; 1717 } 1718 }, { passive: true }); 1719 1720 onScroll(); 1721})(); 1722 1723/*-------------------------------------------------------------- 1724# Case study share â copy link to clipboard 1725--------------------------------------------------------------*/ 1726(function () { 1727 document.addEventListener('click', function (e) { 1728 const btn = e.target.closest ? e.target.closest('.cs-share-copy') : null; 1729 if (!btn) return; 1730 const url = btn.getAttribute('data-url') || window.location.href; 1731 if (navigator.clipboard && navigator.clipboard.writeText) { 1732 navigator.clipboard.writeText(url).then(function () { 1733 btn.classList.add('copied'); 1734 btn.setAttribute('aria-label', 'Link copied'); 1735 setTimeout(function () { 1736 btn.classList.remove('copied'); 1737 btn.setAttribute('aria-label', 'Copy link'); 1738 }, 1500); 1739 }); 1740 } 1741 }); 1742})(); 1743 1744/*-------------------------------------------------------------- 1745# Case study floating sidebar â fixed on the left, visible within the content 1746--------------------------------------------------------------*/ 1747(function () { 1748 const sidebar = document.querySelector('.cs-floating-sidebar'); 1749 if (!sidebar) return; 1750 1751 const startEl = document.querySelector('.about-stats-section') || document.querySelector('.entry-content'); 1752 const endEl = document.querySelector('.entry-content'); 1753 if (!startEl || !endEl) return; 1754 1755 const TOP_GAP = 120; // distance from the top when fixed 1756 1757 function isDesktop() { 1758 return window.innerWidth >= 992; 1759 } 1760 1761 function update() { 1762 if (!isDesktop()) { 1763 sidebar.classList.remove('is-visible'); 1764 return; 1765 } 1766 1767 // Left position is set in CSS (left: 135px). Here we only handle visibility. 1768 // Visibility bounds: from the start of the content to its end 1769 const y = window.pageYOffset || document.documentElement.scrollTop; 1770 const startY = startEl.getBoundingClientRect().top + y - TOP_GAP; 1771 const endY = endEl.getBoundingClientRect().bottom + y - sidebar.offsetHeight - TOP_GAP; 1772 1773 if (y >= startY && y <= endY) { 1774 sidebar.classList.add('is-visible'); 1775 } else { 1776 sidebar.classList.remove('is-visible'); 1777 } 1778 } 1779 1780 let ticking = false; 1781 function onScroll() { 1782 if (!ticking) { 1783 window.requestAnimationFrame(function () { 1784 update(); 1785 ticking = false; 1786 }); 1787 ticking = true; 1788 } 1789 } 1790 1791 window.addEventListener('scroll', onScroll, { passive: true }); 1792 window.addEventListener('resize', update); 1793 update(); 1794})(); 1795 1796/*-------------------------------------------------------------- 1797# Pricing â billing toggle (sync entre bloques + Annually por defecto) 1798--------------------------------------------------------------*/ 1799document.addEventListener('DOMContentLoaded', function () { 1800 var groups = Array.prototype.slice.call(document.querySelectorAll('.billing-toggle-group')); 1801 if (!groups.length) return; 1802 1803 var plans = document.querySelectorAll('.pricingpage #plans .plan'); 1804 var syncing = false;
1805 1806 // Marca cada radio con su ciclo; a los grupos secundarios les da id/for/name únicos 1807 groups.forEach(function (group, gi) { 1808 group.querySelectorAll('input[type="radio"]').forEach(function (input) { 1809 input.setAttribute('data-cycle', input.id.indexOf('monthly') === 0 ? 'monthly' : 'annual'); 1810 if (gi > 0) { 1811 var oldId = input.id; 1812 var label = group.querySelector('label[for="' + oldId + '"]'); 1813 input.id = oldId + '-' + gi; 1814 if (label) label.setAttribute('for', input.id); 1815 input.name = 'billingCycle-' + gi; 1816 } 1817 }); 1818 }); 1819 1820 // Copia los precios calculados del primer bloque a los demás (ids duplicados) 1821 var currentCycle = 'annual'; 1822 1823 function mirrorPricing() { 1824 ['proPrice', 'teamPrice', 'usercount', 'peruserprice', 'calcstorage'].forEach(function (id) { 1825 var els = document.querySelectorAll('[id="' + id + '"]'); 1826 for (var i = 1; i < els.length; i++) els[i].textContent = els[0].textContent; 1827 }); 1828 ['propricingbreakdown', 'teampricingbreakdown'].forEach(function (id) { 1829 var els = document.querySelectorAll('[id="' + id + '"]'); 1830 for (var i = 1; i < els.length; i++) { 1831 els[i].innerHTML = els[0].innerHTML; 1832 els[i].style.display = currentCycle === 'annual' ? 'block' : 'none'; 1833 } 1834 }); 1835 } 1836 1837 // pricing.js calcula de forma asÃncrona (detecta moneda por red); re-espejamos 1838 // cada vez que el primer bloque cambia, para no copiar valores viejos. 1839 if (window.MutationObserver) { 1840 var observer = new MutationObserver(mirrorPricing); 1841 ['proPrice', 'teamPrice', 'usercount', 'peruserprice', 'calcstorage', 'propricingbreakdown', 'teampricingbreakdown'].forEach(function (id) { 1842 var src = document.getElementById(id); 1843 if (src) observer.observe(src, { childList: true, characterData: true, subtree: true }); 1844 }); 1845 } 1846 1847 // En Annually cambia el perÃodo de Pro y Team; en Monthly lo restaura 1848 function updatePeriodText(cycle) { 1849 ['pro', 'team'].forEach(function (planId) { 1850 var blocks = document.querySelectorAll('.pricingpage #plans .plan[id="' + planId + '"]'); 1851 blocks.forEach(function (plan) { 1852 var period = plan.querySelector('.pricing-period'); 1853 if (period) { 1854 period.textContent = cycle === 'annual' 1855 ? 'per month, billed yearly' 1856 : 'per month, billed monthly'; 1857 } 1858 }); 1859 }); 1860 } 1861 1862 function applyCycle(cycle, propagate) { 1863 currentCycle = cycle; 1864 syncing = true; 1865 groups.forEach(function (group) { 1866 group.querySelectorAll('input[type="radio"]').forEach(function (input) { 1867 input.checked = input.getAttribute('data-cycle') === cycle; 1868 }); 1869 }); 1870 plans.forEach(function (plan) { 1871 plan.classList.remove('monthly', 'annually'); 1872 plan.classList.add(cycle === 'monthly' ? 'monthly' : 'annually'); 1873 }); 1874 syncing = false; 1875 1876 if (propagate) { 1877 var first = groups[0].querySelector('input[data-cycle="' + cycle + '"]'); 1878 if (first) first.dispatchEvent(new Event('change', { bubbles: true })); 1879 } 1880 mirrorPricing(); 1881 updatePeriodText(cycle); 1882 } 1883 1884 // Captura: corre antes del stopPropagation() de pricing.js 1885 document.addEventListener('change', function (e) { 1886 if (syncing) return; 1887 var input = e.target; 1888 if (!input || !input.matches || !input.matches('.billing-toggle-group input[type="radio"]')) return; 1889 var fromFirst = groups[0].contains(input); 1890 applyCycle(input.getAttribute('data-cycle'), !fromFirst); 1891 }, true); 1892 1893 // Annually activo por defecto 1894 applyCycle('annual', true); 1895}); 1896/*-------------------------------------------------------------- 1897# Tablas responsive: etiquetas de cada celda desde el encabezado real 1898--------------------------------------------------------------*/ 1899document.addEventListener('DOMContentLoaded', function () { 1900 document.querySelectorAll('.wp-block-table table').forEach(function (table) { 1901 var headerCells = table.querySelectorAll('thead tr:first-child th, thead tr:first-child td'); 1902 var bodyRows = Array.prototype.slice.call(table.querySelectorAll('tbody tr')); 1903 1904 // Tablas insertadas sin "Header section": la primera fila del tbody hace de encabezado 1905 if (!headerCells.length && bodyRows.length) { 1906 var headerRow = bodyRows.shift(); 1907 headerRow.setAttribute('data-table-header', ''); 1908 headerCells = headerRow.querySelectorAll('th, td'); 1909 } 1910 1911 if (!headerCells.length) return; 1912 1913 var labels = Array.prototype.map.call(headerCells, function (cell) { 1914 return cell.textContent.replace(/\s+/g, ' ').trim(); 1915 }); 1916 1917 bodyRows.forEach(function (row) { 1918 row.querySelectorAll('td').forEach(function (cell, index) { 1919 if (labels[index]) cell.setAttribute('data-label', labels[index]); 1920 }); 1921 }); 1922 }); 1923}); 1924
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.