1/** 2 * FAQ Elementor Page Widget - JavaScript 3 */ 4 5(function($) { 6 'use strict'; 7 8 // Debounce function 9 function debounce(func, wait) { 10 let timeout; 11 return function executedFunction(...args) { 12 const later = () => { 13 clearTimeout(timeout); 14 func(...args); 15 }; 16 clearTimeout(timeout); 17 timeout = setTimeout(later, wait); 18 }; 19 } 20 21 // Initialize Sticky Categories 22 function initStickyCategories($container) { 23 // Check if sticky is enabled 24 const stickyEnabled = $container.data('sticky-categories'); 25 if (stickyEnabled !== 'yes') return; 26 27 const $categoriesTabs = $container.find('.faq-page-categories-tabs'); 28 29 if (!$categoriesTabs.length) return; 30 31 // Create placeholder element to maintain layout when sticky 32 const $placeholder = $('<div class="faq-page-categories-placeholder"></div>'); 33 $categoriesTabs.after($placeholder); 34 35 // Get background color from container or parent 36 let bgColor = $container.find('.faq-page-main-content').css('background-color'); 37 if (bgColor === 'rgba(0, 0, 0, 0)' || bgColor === 'transparent') { 38 bgColor = $container.css('background-color'); 39 } 40 if (bgColor === 'rgba(0, 0, 0, 0)' || bgColor === 'transparent') { 41 bgColor = $container.closest('section, .elementor-section, .elementor-widget-container').css('background-color'); 42 } 43 if (bgColor === 'rgba(0, 0, 0, 0)' || bgColor === 'transparent') { 44 bgColor = '#1a1a2e'; // Fallback color 45 } 46 47 let originalTop = $categoriesTabs.offset().top; 48 let originalHeight = $categoriesTabs.outerHeight(); 49 let isSticky = false; 50 51 $(window).on('scroll.faqPageSticky', function() { 52 const scrollTop = $(window).scrollTop(); 53 54 // Check if we should make it sticky - just lock at top, no animation 55 if (scrollTop >= originalTop && !isSticky) { 56 isSticky = true; 57 $categoriesTabs.addClass('is-sticky'); 58 $categoriesTabs.css('background-color', bgColor); 59 $placeholder.css('height', originalHeight + 'px'); 60 $placeholder.addClass('is-visible'); 61 } else if (scrollTop < originalTop && isSticky) { 62 isSticky = false; 63 $categoriesTabs.removeClass('is-sticky'); 64 $categoriesTabs.css('background-color', ''); 65 $placeholder.removeClass('is-visible'); 66 } 67 }); 68 69 // Recalculate on resize 70 $(window).on('resize.faqPageSticky', debounce(function() { 71 if (!isSticky) { 72 originalTop = $categoriesTabs.offset().top; 73 originalHeight = $categoriesTabs.outerHeight(); 74 } 75 }, 100)); 76 } 77 78 // Initialize Category Slider with Swiper 79 function initCategorySlider($container) { 80 const $slider = $container.find('.faq-page-category-slider'); 81 const $swiperEl = $slider.find('.faq-page-swiper'); 82 83 if (!$swiperEl.length) return; 84 85 // Check if Swiper is available 86 if (typeof Swiper === 'undefined') { 87 console.warn('Swiper not loaded'); 88 return; 89 } 90 91 const $nextBtn = $slider.find('.faq-page-swiper-next'); 92 const $categoriesTabs = $container.find('.faq-page-categories-tabs'); 93 94 // Initialize Swiper - começando do inÃcio (esquerda) 95 const swiper = new Swiper($swiperEl[0], { 96 slidesPerView: 'auto', 97 spaceBetween: 10, 98 freeMode: true, 99 grabCursor: true, 100 watchOverflow: true, 101 initialSlide: 0, // Começa no primeiro slide (esquerda) 102 navigation: { 103 nextEl: $nextBtn[0], 104 }, 105 // Configurações de touch 106 touchRatio: 1, 107 touchAngle: 45, 108 simulateTouch: true, 109 shortSwipes: true, 110 longSwipesRatio: 0.5,
111 // Breakpoints para responsividade 112 breakpoints: { 113 320: { 114 spaceBetween: 8, 115 }, 116 768: { 117 spaceBetween: 10, 118 }, 119 1024: { 120 spaceBetween: 12, 121 } 122 } 123 }); 124 125 // Store swiper instance on container for later access 126 $container.data('category-swiper', swiper); 127 128 // Update swiper on window resize 129 $(window).on('resize', debounce(function() { 130 if (swiper && swiper.update) { 131 swiper.update(); 132 } 133 }, 100)); 134 } 135 136 // Initialize FAQ Page Widget 137 function initFAQPageWidget($container) { 138 if (!$container.length || $container.data('faq-page-initialized')) { 139 return; 140 } 141 142 $container.data('faq-page-initialized', true); 143 144 // Initialize category slider (Swiper) 145 initCategorySlider($container); 146 147 // Initialize sticky categories 148 initStickyCategories($container); 149 150 const widgetId = $container.data('widget-id'); 151 const postsPerPage = parseInt($container.data('posts-per-page')) || 10; 152 const paginationType = $container.data('pagination-type') || 'load_more'; 153 const allowMultiple = $container.data('allow-multiple') === 'yes'; 154 let maxPages = parseInt($container.data('max-pages')) || 1; 155 156 let currentPage = 1; 157 let currentCategory = 'all'; 158 let currentSearch = ''; 159 let isLoading = false; 160 161 const $searchInput = $container.find('.faq-page-search-input'); 162 const $searchClear = $container.find('.faq-page-search-clear'); 163 const $searchIcon = $container.find('.faq-page-search-icon'); 164 const $searchLoading = $container.find('.faq-page-search-loading'); 165 const $categoryBtns = $container.find('.faq-page-category-btn'); 166 const $itemsWrapper = $container.find('.faq-page-items-wrapper'); 167 const $itemsContainer = $container.find('.faq-page-items'); 168 const $noResults = $container.find('.faq-page-search-no-results'); 169 const $loadMoreBtn = $container.find('.faq-page-load-more-btn'); 170 const $paginationNums = $container.find('.faq-page-pagination-num'); 171 172 // Esconder botão "carregar mais" se só tem uma página no carregamento inicial 173 if ($loadMoreBtn.length && maxPages <= 1) { 174 $loadMoreBtn.hide(); 175 } 176 177 // Toggle FAQ item 178 function toggleItem($item) { 179 const isActive = $item.hasClass('active'); 180 181 if (!allowMultiple) { 182 // Close all other items 183 $itemsContainer.find('.faq-page-item.active').not($item).removeClass('active'); 184 } 185 186 $item.toggleClass('active', !isActive); 187 } 188 189 // FAQ item click handler 190 $container.on('click', '.faq-page-question-wrapper', function() { 191 const $item = $(this).closest('.faq-page-item'); 192 toggleItem($item); 193 }); 194 195 // Keyboard accessibility 196 $container.on('keydown', '.faq-page-question-wrapper', function(e) { 197 if (e.key === 'Enter' || e.key === ' ') { 198 e.preventDefault(); 199 const $item = $(this).closest('.faq-page-item'); 200 toggleItem($item); 201 } 202 }); 203 204 // Search functionality - busca em tempo real 205 const performSearch = debounce(function(query) { 206 currentSearch = query.trim(); 207 currentPage = 1; 208 209 if (currentSearch.length === 0) { 210 $searchClear.hide(); 211 removeHighlights(); 212 resetToInitial(); 213 return; 214 } 215 216 $searchClear.show(); 217 218 // Primeiro faz filtro local para resposta imediata 219 filterAndHighlightLocal(currentSearch); 220 221 // Depois busca no servidor para resultados completos 222 loadFAQs(true); 223 }, 150); 224 225 // Filtro e highlight local para resposta imediata 226 function filterAndHighlightLocal(search) { 227 if (!search || search.length < 2) return; 228 229 const searchLower = search.toLowerCase(); 230 const terms = search.split(/\s+/).filter(t => t.length > 1); 231 232 $itemsContainer.find('.faq-page-item').each(function() { 233 const $item = $(this); 234 const questionText = $item.find('.faq-page-question').text().toLowerCase(); 235 const answerText = $item.find('.faq-page-answer-content').text().toLowerCase(); 236 237 // Verifica se algum termo aparece na pergunta ou resposta 238 const matches = terms.some(term =>
239 questionText.includes(term.toLowerCase()) || 240 answerText.includes(term.toLowerCase()) 241 ); 242 243 if (matches) { 244 $item.removeClass('hidden').show(); 245 } else { 246 $item.addClass('hidden').hide(); 247 } 248 }); 249 250 // Aplica highlight 251 highlightSearchTerms(search); 252 253 // Verifica se há resultados visÃveis 254 const visibleItems = $itemsContainer.find('.faq-page-item:not(.hidden)').length; 255 if (visibleItems === 0) { 256 $noResults.show(); 257 } else { 258 $noResults.hide(); 259 } 260 } 261 262 // Remove highlights 263 function removeHighlights() { 264 $itemsContainer.find('.faq-page-highlight').each(function() { 265 const $highlight = $(this); 266 $highlight.replaceWith($highlight.text()); 267 }); 268 } 269 270 $searchInput.on('input', function() { 271 const query = $(this).val(); 272 performSearch(query); 273 }); 274 275 // Clear search 276 $searchClear.on('click', function() { 277 $searchInput.val(''); 278 currentSearch = ''; 279 $searchClear.hide(); 280 removeHighlights(); 281 resetToInitial(); 282 }); 283 284 // Category filter 285 $categoryBtns.on('click', function() { 286 const $btn = $(this); 287 const category = $btn.data('category'); 288 289 // Update active state 290 $categoryBtns.removeClass('active'); 291 $btn.addClass('active'); 292 293 // Set category and reload 294 currentCategory = category; 295 currentPage = 1; 296 297 // Remove highlights when changing category 298 removeHighlights(); 299 // Clear search when changing category 300 if (currentSearch) { 301 $searchInput.val(''); 302 currentSearch = ''; 303 $searchClear.hide(); 304 } 305 306 loadFAQs(true); 307 308 // Track tag click 309 if (category !== 'all' && typeof faqElementor !== 'undefined') { 310 $.post(faqElementor.ajaxUrl, { 311 action: 'faq_track_tag', 312 nonce: faqElementor.nonce, 313 tag: category, 314 lang: faqElementor.lang || '' 315 }); 316 } 317 }); 318 319 // Load more button 320 $loadMoreBtn.on('click', function() { 321 if (isLoading) return; 322 if (currentPage >= maxPages) { 323 $loadMoreBtn.hide(); 324 return; 325 } 326 currentPage++; 327 loadFAQs(false); 328 }); 329 330 // Pagination numbers 331 $paginationNums.on('click', function() { 332 const page = parseInt($(this).data('page')); 333 if (isLoading || page === currentPage) return; 334 335 $paginationNums.removeClass('active'); 336 $(this).addClass('active'); 337 338 currentPage = page; 339 loadFAQs(true); 340 341 // Scroll to top of FAQ items 342 $('html, body').animate({ 343 scrollTop: $itemsWrapper.offset().top - 100 344 }, 300); 345 }); 346 347 // Infinite scroll 348 if (paginationType === 'infinite') { 349 let scrollTimeout; 350 $(window).on('scroll', function() { 351 clearTimeout(scrollTimeout); 352 scrollTimeout = setTimeout(function() { 353 if (isLoading || currentPage >= maxPages) return; 354 355 const scrollPos = $(window).scrollTop() + $(window).height(); 356 const triggerPos = $itemsWrapper.offset().top + $itemsWrapper.height() - 200; 357 358 if (scrollPos >= triggerPos) { 359 currentPage++; 360 loadFAQs(false); 361 } 362 }, 100); 363 }); 364 } 365 366 // Load FAQs via AJAX 367 function loadFAQs(replace) { 368 if (isLoading) return; 369 isLoading = true; 370 371 // Show loading state 372 $itemsWrapper.addClass('loading'); 373 $searchIcon.hide(); 374 $searchLoading.show(); 375 376 if ($loadMoreBtn.length) { 377 $loadMoreBtn.prop('disabled', true); 378 $loadMoreBtn.find('.faq-page-load-more-text').hide(); 379 $loadMoreBtn.find('.faq-page-load-more-loading').show(); 380 $loadMoreBtn.find('.faq-page-load-more-spinner').show(); 381 } 382 383 const data = { 384 action: 'faq_page_search', 385 nonce: faqElementor.nonce, 386 search: currentSearch, 387 category: currentCategory, 388 page: currentPage, 389 posts_per_page: postsPerPage, 390 lang: faqElementor.lang || '' 391 }; 392 393 $.post(faqElementor.ajaxUrl, data, function(response) { 394 isLoading = false; 395 $itemsWrapper.removeClass('loading'); 396 $searchIcon.show(); 397 $searchLoading.hide(); 398 399 if ($loadMoreBtn.length) { 400 $loadMoreBtn.prop('disabled', false); 401 $loadMoreBtn.find('.faq-page-load-more-text').show(); 402 $loadMoreBtn.find('.faq-page-load-more-loading').hide(); 403 $loadMoreBtn.find('.faq-page-load-more-spinner').hide(); 404 } 405 406 if (response.success) { 407 const items = response.data.items; 408 const hasMore = response.data.has_more; 409 const maxPagesNew = response.data.max_pages; 410 411 // Atualiza maxPages global 412 maxPages = maxPagesNew; 413 414 if (replace) { 415 $itemsContainer.empty(); 416 } 417 418 if (items.length === 0 && replace) { 419 $itemsContainer.hide(); 420 $noResults.show(); 421 // Esconde botão se não há resultados 422 if ($loadMoreBtn.length) { 423 $loadMoreBtn.hide(); 424 } 425 } else { 426 $noResults.hide(); 427 $itemsContainer.show(); 428
429 items.forEach(function(item) { 430 const $newItem = createFAQItem(item); 431 $itemsContainer.append($newItem); 432 }); 433 434 // Highlight search terms 435 if (currentSearch) { 436 highlightSearchTerms(currentSearch); 437 } 438 439 // Update load more button visibility 440 if ($loadMoreBtn.length) { 441 // Esconde se não há mais páginas OU se chegamos na última página 442 if (!hasMore || currentPage >= maxPages) { 443 $loadMoreBtn.hide(); 444 } else { 445 $loadMoreBtn.show(); 446 } 447 } 448 } 449 450 // Update pagination numbers 451 updatePaginationNumbers(maxPagesNew); 452 } 453 }).fail(function() { 454 isLoading = false; 455 $itemsWrapper.removeClass('loading'); 456 $searchIcon.show(); 457 $searchLoading.hide(); 458 459 if ($loadMoreBtn.length) { 460 $loadMoreBtn.prop('disabled', false); 461 $loadMoreBtn.find('.faq-page-load-more-text').show(); 462 $loadMoreBtn.find('.faq-page-load-more-loading').hide(); 463 $loadMoreBtn.find('.faq-page-load-more-spinner').hide(); 464 } 465 }); 466 } 467 468 // Create FAQ item HTML 469 function createFAQItem(item) { 470 const tagsAttr = item.tags ? item.tags.join(',') : ''; 471 const $item = $(` 472 <div class="faq-page-item" data-id="${item.id}" data-tags="${tagsAttr}"> 473 <div class="faq-page-question-wrapper" tabindex="0" role="button" aria-expanded="false"> 474 <span class="faq-page-question">${escapeHtml(item.title)}</span> 475 <span class="faq-page-icon"> 476 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 477 <polyline points="6 9 12 15 18 9"></polyline> 478 </svg> 479 </span> 480 </div> 481 <div class="faq-page-answer"> 482 <div class="faq-page-answer-content"> 483 ${item.content} 484 </div> 485 </div> 486 </div> 487 `); 488 return $item; 489 } 490 491 // Escape HTML 492 function escapeHtml(text) { 493 const div = document.createElement('div'); 494 div.textContent = text; 495 return div.innerHTML; 496 } 497 498 // Highlight search terms 499 function highlightSearchTerms(search) { 500 const terms = search.split(/\s+/).filter(t => t.length > 2); 501 if (terms.length === 0) return; 502 503 const regex = new RegExp('(' + terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')', 'gi'); 504 505 $itemsContainer.find('.faq-page-question, .faq-page-answer-content').each(function() { 506 const $el = $(this); 507 const html = $el.html(); 508 509 // Only highlight text nodes, not HTML tags 510 const highlighted = html.replace(/>([^<]+)</g, function(match, text) { 511 return '>' + text.replace(regex, '<span class="faq-page-highlight">$1</span>') + '<'; 512 }); 513 514 $el.html(highlighted); 515 }); 516 } 517 518 // Update pagination numbers 519 function updatePaginationNumbers(newMaxPages) { 520 const $paginationContainer = $container.find('.faq-page-pagination-numbers'); 521 if (!$paginationContainer.length) return; 522 523 $paginationContainer.empty(); 524 525 for (let i = 1; i <= newMaxPages; i++) { 526 const activeClass = i === currentPage ? 'active' : ''; 527 $paginationContainer.append(` 528 <button type="button" class="faq-page-pagination-num ${activeClass}" data-page="${i}">${i}</button> 529 `); 530 } 531 532 // Re-bind click events 533 $paginationContainer.find('.faq-page-pagination-num').on('click', function() { 534 const page = parseInt($(this).data('page')); 535 if (isLoading || page === currentPage) return; 536 537 $paginationContainer.find('.faq-page-pagination-num').removeClass('active'); 538 $(this).addClass('active'); 539 540 currentPage = page; 541 loadFAQs(true); 542 543 $('html, body').animate({ 544 scrollTop: $itemsWrapper.offset().top - 100 545 }, 300); 546 }); 547 } 548 549 // Reset to initial state 550 function resetToInitial() { 551 currentCategory = 'all'; 552 currentPage = 1; 553 554 // Reset category buttons 555 $categoryBtns.removeClass('active'); 556 $categoryBtns.filter('[data-category="all"]').addClass('active'); 557 558 // Remove highlights 559 removeHighlights();
560 561 // Show all items 562 $itemsContainer.find('.faq-page-item').removeClass('hidden').show(); 563 $noResults.hide(); 564 565 loadFAQs(true); 566 } 567 568 // Make question wrappers focusable 569 $container.find('.faq-page-question-wrapper').attr({ 570 'tabindex': '0', 571 'role': 'button', 572 'aria-expanded': 'false' 573 }); 574 575 // Update aria-expanded on toggle 576 $container.on('click', '.faq-page-question-wrapper', function() { 577 const $item = $(this).closest('.faq-page-item'); 578 const isExpanded = $item.hasClass('active'); 579 $(this).attr('aria-expanded', isExpanded); 580 }); 581 } 582 583 // Initialize on document ready 584 $(document).ready(function() { 585 $('.faq-page-container').each(function() { 586 initFAQPageWidget($(this)); 587 }); 588 }); 589 590 // Initialize for Elementor frontend 591 $(window).on('elementor/frontend/init', function() { 592 if (typeof elementorFrontend !== 'undefined') { 593 elementorFrontend.hooks.addAction('frontend/element_ready/faq_pda_page_widget.default', function($scope) { 594 initFAQPageWidget($scope.find('.faq-page-container')); 595 }); 596 } 597 }); 598 599})(jQuery);
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.