1/** 2 * RSS Feeds Display Plugin Frontend JavaScript 3 * Handles carousel functionality, lazy loading, and dynamic interactions 4 */ 5 6(function($) { 7 'use strict'; 8 9 // Main plugin object 10 const RSSFeedsDisplay = { 11 12 // Initialize all functionality 13 init: function() { 14 this.initCarousels(); 15 this.initLazyLoading(); 16 this.initLoadMore(); 17 this.initSearch(); 18 this.initViewSwitcher(); 19 this.initPagination(); 20 this.bindEvents(); 21 }, 22 23 // Initialize carousel functionality 24 initCarousels: function() { 25 // Initialize Swiper-based carousels for both carousel and slider formats 26 $('.rss-feeds-carousel[data-view-format="carousel"]:not(.carousel-initialized), .rss-feeds-slider[data-view-format="slider"]:not(.carousel-initialized)').each(function() { 27 const $container = $(this); 28 $container.addClass('carousel-initialized'); 29 30 // Create instance object for RSSFeedsCarousel 31 const instance = { 32 container: $container[0], 33 config: { 34 viewFormat: $container.data('view-format'), 35 carouselDraggable: $container.data('carousel-draggable') === true || $container.data('carousel-draggable') === 'true', 36 carouselAutoplay: $container.data('carousel-autoplay') === true || $container.data('carousel-autoplay') === 'true', 37 carouselArrows: $container.data('carousel-arrows') !== false && $container.data('carousel-arrows') !== 'false', 38 carouselDots: $container.data('carousel-dots') !== false && $container.data('carousel-dots') !== 'false', 39 } 40 }; 41 42 // Initialize the Swiper-based carousel 43 if (typeof RSSFeedsCarousel !== 'undefined') { 44 const carousel = new RSSFeedsCarousel(instance); 45 } 46 }); 47 }, 48 49 // Initialize lazy loading for images 50 initLazyLoading: function() { 51 if ('IntersectionObserver' in window) { 52 if (!this.imageObserver) { 53 this.imageObserver = new IntersectionObserver((entries, observer) => { 54 entries.forEach(entry => { 55 if (entry.isIntersecting) { 56 const img = entry.target; 57 img.src = img.dataset.src; 58 img.classList.remove('lazy'); 59 observer.unobserve(img); 60 } 61 }); 62 }); 63 } 64 65 document.querySelectorAll('img[data-src]:not(.lazy-observed)').forEach(img => { 66 this.imageObserver.observe(img); 67 img.classList.add('lazy-observed'); 68 }); 69 } else { 70 // Fallback for older browsers 71 $('img[data-src]').each(function() { 72 const $img = $(this); 73 $img.attr('src', $img.attr('data-src')); 74 }); 75 } 76 }, 77 78 // Initialize load more functionality 79 initLoadMore: function() { 80 $('.rss-load-more').on('click', function(e) { 81 e.preventDefault(); 82 const $button = $(this); 83 const $container = $button.closest('.rss-feeds-container'); 84 const feedId = $container.data('feed-id'); 85 const offset = $container.find('.rss-feed-item').length; 86 const limit = parseInt($button.data('limit')) || 10; 87 88 RSSFeedsDisplay.loadMoreItems($button, feedId, offset, limit); 89 }); 90 }, 91 92 // Initialize search functionality 93 initSearch: function() { 94 $('.rss-search-form').on('submit', function(e) { 95 e.preventDefault(); 96 const $form = $(this); 97 const $input = $form.find('.rss-search-input'); 98 const $container = $form.closest('.rss-feeds-container'); 99 const feedId = $container.data('feed-id'); 100 const searchQuery = $input.val().trim(); 101 102 RSSFeedsDisplay.searchItems($container, feedId, searchQuery); 103 }); 104 105 // Real-time search with debounce 106 $('.rss-search-input').on('input', RSSFeedsDisplay.debounce(function() { 107 const $input = $(this); 108 const $container = $input.closest('.rss-feeds-container'); 109 const feedId = $container.data('feed-id'); 110 const searchQuery = $input.val().trim(); 111 112 if (searchQuery.length >= 3 || searchQuery.length === 0) { 113 RSSFeedsDisplay.searchItems($container, feedId, searchQuery); 114 } 115 }, 500)); 116 }, 117 118 // Initialize view switcher 119 initViewSwitcher: function() { 120 $('.rss-view-switcher button').on('click', function(e) { 121 e.preventDefault(); 122 const $button = $(this); 123 const $container = $button.closest('.rss-feeds-container'); 124 const feedId = $container.data('feed-id'); 125 const viewFormat = $button.data('view'); 126 127 if (!$button.hasClass('active')) { 128 RSSFeedsDisplay.switchView($container, feedId, viewFormat); 129 } 130 }); 131 }, 132 133 // Bind additional events 134 bindEvents: function() { 135 // Refresh feed button 136 $('.rss-refresh-feed').on('click', function(e) { 137 e.preventDefault(); 138 const $button = $(this); 139 const $container = $button.closest('.rss-feeds-container'); 140 const feedId = $container.data('feed-id'); 141 142 RSSFeedsDisplay.refreshFeed($container, feedId); 143 }); 144 145 // Share functionality 146 $('.rss-share-item').on('click', function(e) { 147 e.preventDefault(); 148 const url = $(this).data('url'); 149 const title = $(this).data('title'); 150 RSSFeedsDisplay.shareItem(url, title); 151 }); 152 }, 153 154 // Load more items via AJAX
155 loadMoreItems: function($button, feedId, offset, limit) { 156 const originalText = $button.text(); 157 const nonce = $button.closest('.rss-feeds-grid, .rss-feeds-list, .rss-feeds-carousel').data('nonce'); 158 $button.text(rssFeedsDisplay.strings.loading).prop('disabled', true); 159 160 $.post(rssFeedsDisplay.ajax_url, { 161 action: 'rss_feeds_load_more', 162 feed_id: feedId, 163 offset: offset, 164 limit: limit, 165 nonce: nonce 166 }) 167 .done(function(response) { 168 if (response.success) { 169 const $container = $button.siblings('.rss-feeds-content'); 170 const $newItems = $(response.data.html); 171 172 // Add fade-in animation 173 $newItems.addClass('rss-fade-in'); 174 $container.append($newItems); 175 176 // Reinitialize lazy loading for new images 177 RSSFeedsDisplay.initLazyLoading(); 178 179 // Hide button if no more items 180 if (!response.data.has_more) { 181 $button.hide(); 182 } 183 } else { 184 RSSFeedsDisplay.showError($button.parent(), response.data?.message || rssFeedsDisplay.strings.error); 185 } 186 }) 187 .fail(function() { 188 RSSFeedsDisplay.showError($button.parent(), rssFeedsDisplay.strings.error); 189 }) 190 .always(function() { 191 $button.text(originalText).prop('disabled', false); 192 }); 193 }, 194 195 // Search items via AJAX 196 searchItems: function($container, feedId, searchQuery) { 197 const $content = $container.find('.rss-feeds-content'); 198 const $loadingIndicator = $('<div class="rss-feeds-loading">' + rssFeedsDisplay.strings.loading + '</div>'); 199 const nonce = $container.data('nonce'); 200 201 $content.html($loadingIndicator); 202 203 $.post(rssFeedsDisplay.ajax_url, { 204 action: 'rss_feeds_search', 205 feed_id: feedId, 206 search: searchQuery, 207 nonce: nonce 208 }) 209 .done(function(response) { 210 if (response.success) { 211 $content.html(response.data.html); 212 RSSFeedsDisplay.initLazyLoading(); 213 214 // Update search results count 215 const $resultsCount = $container.find('.rss-search-results-count'); 216 if ($resultsCount.length) { 217 const count = response.data.total_found || 0; 218 $resultsCount.text(count + ' ' + (count === 1 ? 'result' : 'results')); 219 } 220 } else { 221 RSSFeedsDisplay.showError($content, response.data?.message || rssFeedsDisplay.strings.error); 222 } 223 }) 224 .fail(function() { 225 RSSFeedsDisplay.showError($content, rssFeedsDisplay.strings.error); 226 }); 227 }, 228 229 // Switch view format via AJAX 230 switchView: function($container, feedId, viewFormat) { 231 const $content = $container.find('.rss-feeds-content'); 232 const $buttons = $container.find('.rss-view-switcher button'); 233 const $loadingIndicator = $('<div class="rss-feeds-loading">' + rssFeedsDisplay.strings.loading + '</div>'); 234 const nonce = $container.data('nonce'); 235 236 $content.html($loadingIndicator); 237 $buttons.removeClass('active'); 238 $buttons.filter('[data-view="' + viewFormat + '"]').addClass('active'); 239 240 $.post(rssFeedsDisplay.ajax_url, { 241 action: 'rss_feeds_update_view', 242 feed_id: feedId, 243 view_format: viewFormat, 244 nonce: nonce 245 }) 246 .done(function(response) { 247 if (response.success) { 248 $content.html(response.data.html); 249 250 // Reinitialize components for new view 251 if (viewFormat === 'carousel' || viewFormat === 'slider') { 252 RSSFeedsDisplay.initCarousels(); 253 } 254 RSSFeedsDisplay.initLazyLoading(); 255 } else { 256 RSSFeedsDisplay.showError($content, response.data?.message || rssFeedsDisplay.strings.error); 257 } 258 }) 259 .fail(function() { 260 RSSFeedsDisplay.showError($content, rssFeedsDisplay.strings.error); 261 }); 262 }, 263 264 // Refresh feed data 265 refreshFeed: function($container, feedId) { 266 const $button = $container.find('.rss-refresh-feed'); 267 const $content = $container.find('.rss-feeds-content'); 268 const originalText = $button.text(); 269 270 $button.text(rssFeedsDisplay.strings.loading).prop('disabled', true); 271 272 $.post(rssFeedsDisplay.ajax_url, { 273 action: 'rss_feeds_refresh_feed', 274 feed_id: feedId, 275 nonce: rssFeedsDisplay.nonce 276 }) 277 .done(function(response) { 278 if (response.success) { 279 // Reload the page to show fresh content 280 location.reload(); 281 } else { 282 RSSFeedsDisplay.showError($content, response.data?.message || rssFeedsDisplay.strings.error); 283 } 284 }) 285 .fail(function() { 286 RSSFeedsDisplay.showError($content, rssFeedsDisplay.strings.error); 287 }) 288 .always(function() { 289 $button.text(originalText).prop('disabled', false); 290 }); 291 }, 292 293 // Share item functionality 294 shareItem: function(url, title) { 295 if (navigator.share) { 296 navigator.share({ 297 title: title, 298 url: url 299 }).catch(console.error); 300 } else { 301 // Fallback: copy to clipboard 302 const textArea = document.createElement('textarea'); 303 textArea.value = url; 304 document.body.appendChild(textArea); 305 textArea.focus(); 306 textArea.select(); 307 308 try { 309 document.execCommand('copy'); 310 alert('Link copied to clipboard!'); 311 } catch (err) { 312 console.error('Could not copy link: ', err); 313 // Fallback: show URL in prompt 314 prompt('Copy this link:', url); 315 } 316 317 document.body.removeChild(textArea); 318 } 319 }, 320 321 // Show error message 322 showError: function($container, message) { 323 const $error = $('<div class="rss-feeds-error">' + message + '</div>'); 324 $container.html($error); 325 }, 326 327 // Initialize pagination functionality 328 initPagination: function() { 329 // Handle pagination clicks
330 $(document).on('click', '.rss-pagination .rss-page-link:not(.rss-current):not(.rss-disabled)', function(e) { 331 e.preventDefault(); 332 333 const $link = $(this); 334 const page = parseInt($link.data('page')); 335 const $pagination = $link.closest('.rss-pagination'); 336 const feedId = $pagination.data('feed-id'); 337 338 // Find the container to get the actual instance ID 339 const $container = $pagination.closest('[id^="rss-feeds-"]'); 340 const containerId = $container.attr('id'); 341 342 if (page && feedId && containerId && !$link.hasClass('rss-current')) { 343 // Debug logging (can be removed in production) 344 if (window.console) { 345 console.log('RSS Feeds: Loading page', page, 'for feed', feedId); 346 } 347 RSSFeedsDisplay.loadPage(feedId, page, containerId); 348 } else { 349 // Debug: Log missing data 350 if (window.console && (!page || !feedId || !containerId)) { 351 console.log('RSS Feeds: Missing required data:', { page, feedId, containerId }); 352 } 353 } 354 }); 355 }, 356 357 // Load specific page via AJAX 358 loadPage: function(feedId, page, containerId) { 359 // Use provided container ID or find it 360 const $container = containerId ? $('#' + containerId) : $('[id^="rss-feeds-' + feedId + '-"]').first(); 361 if (!$container.length) { 362 console.error('RSS Feeds: Container not found for feed', feedId); 363 return; 364 } 365 366 const $content = $container.find('.rss-feeds-grid, .rss-feeds-list').first(); 367 const $pagination = $container.find('.rss-pagination'); 368 const nonce = $container.data('nonce'); 369 const viewFormat = $container.data('view-format'); 370 371 if (!$content.length || !nonce) { 372 console.error('RSS Feeds: Missing content or nonce for pagination'); 373 return; 374 } 375 376 // Add loading state 377 $container.addClass('rss-feeds-loading'); 378 $pagination.addClass('rss-pagination-loading'); 379 380 // Disable all pagination links during loading 381 $('.rss-page-link').addClass('rss-disabled'); 382 383 // Scroll to top if enabled 384 if (rssFeedsDisplay.scrollToTop !== false) { 385 $('html, body').animate({ 386 scrollTop: $container.offset().top - 50 387 }, 300); 388 } 389 390 $.post(rssFeedsDisplay.ajax_url, { 391 action: 'rss_feeds_load_page', 392 feed_id: feedId, 393 page: page, 394 view_format: viewFormat, 395 instance_id: containerId || ('rss-feeds-' + feedId + '-1'), 396 nonce: nonce 397 }) 398 .done(function(response) { 399 if (response.success && response.data.content) { 400 // Replace only the wrapper content (pagination + items) 401 $container.fadeOut(200, function() { 402 $(this).html($(response.data.content).html()).fadeIn(200); 403 404 // Reinitialize components 405 RSSFeedsDisplay.initLazyLoading(); 406 407 // Update URL without refreshing page (if supported) 408 if (history.pushState && page > 1) { 409 const url = new URL(window.location); 410 url.searchParams.set(feedId + '_page', page); 411 history.pushState(null, '', url); 412 } else if (history.pushState && page === 1) { 413 const url = new URL(window.location); 414 url.searchParams.delete(feedId + '_page'); 415 history.pushState(null, '', url); 416 } 417 }); 418 } else { 419 RSSFeedsDisplay.showError($content, response.data?.message || rssFeedsDisplay.strings.error); 420 } 421 }) 422 .fail(function() { 423 RSSFeedsDisplay.showError($content, rssFeedsDisplay.strings.error); 424 }) 425 .always(function() { 426 $container.removeClass('rss-feeds-loading'); 427 $pagination.removeClass('rss-pagination-loading'); 428 // Re-enable pagination links 429 $('.rss-page-link').removeClass('rss-disabled'); 430 }); 431 }, 432 433 // Debounce utility function 434 debounce: function(func, wait, immediate) { 435 let timeout; 436 return function() { 437 const context = this; 438 const args = arguments; 439 const later = function() { 440 timeout = null;
441 if (!immediate) func.apply(context, args); 442 }; 443 const callNow = immediate && !timeout; 444 clearTimeout(timeout); 445 timeout = setTimeout(later, wait); 446 if (callNow) func.apply(context, args); 447 }; 448 } 449 }; 450 451 // Carousel class 452 class RSSCarousel { 453 constructor($element) { 454 this.$carousel = $element; 455 this.$track = $element.find('.rss-carousel-track'); 456 this.$slides = $element.find('.rss-carousel-slide'); 457 this.$prevBtn = $element.find('.rss-carousel-prev'); 458 this.$nextBtn = $element.find('.rss-carousel-next'); 459 this.$dots = $element.find('.rss-carousel-dot'); 460 461 this.currentSlide = 0; 462 this.totalSlides = this.$slides.length; 463 this.autoplay = $element.data('autoplay') === true; 464 this.autoplayInterval = null; 465 this.touchStartX = 0; 466 this.touchEndX = 0; 467 this.isTransitioning = false; 468 469 // Detectar el modo basándose en la configuración del elemento 470 this.viewFormat = $element.data('view-format'); 471 this.itemsPerView = $element.data('items-per-view'); 472 this.isCarouselMode = this.viewFormat === 'carousel' && this.itemsPerView === 'auto'; 473 this.isSliderMode = this.viewFormat === 'slider' && this.itemsPerView === 1; 474 475 if (this.isCarouselMode) { 476 this.visibleSlides = this.calculateVisibleSlides(); 477 } else { 478 this.visibleSlides = 1; // Modo slider: siempre 1 elemento por vez 479 } 480 } 481 482 init() { 483 if (this.totalSlides <= 1) { 484 this.$prevBtn.hide(); 485 this.$nextBtn.hide(); 486 this.$dots.parent().hide(); 487 return; 488 } 489 490 // Configurar el modo apropiado 491 if (this.isCarouselMode) { 492 this.setupCarouselMode(); 493 494 // Recalcular en resize solo para modo carousel (debounced for performance) 495 $(window).on('resize', RSSFeedsDisplay.debounce(() => { 496 this.calculateVisibleSlides(); 497 this.updateCarouselMode(); 498 }, 250)); 499 } else if (this.isSliderMode) { 500 this.setupSliderMode(); 501 } 502 503 this.bindEvents(); 504 this.updateNavigation(); 505 506 if (this.autoplay) { 507 this.startAutoplay(); 508 } 509 } 510 511 bindEvents() { 512 // Navigation buttons 513 this.$prevBtn.on('click', () => this.previousSlide()); 514 this.$nextBtn.on('click', () => this.nextSlide()); 515 516 // Dots navigation 517 this.$dots.on('click', (e) => { 518 const slideIndex = parseInt($(e.target).data('slide')); 519 this.goToSlide(slideIndex); 520 }); 521 522 // Touch/swipe support 523 this.$carousel.on('touchstart', (e) => this.handleTouchStart(e)); 524 this.$carousel.on('touchend', (e) => this.handleTouchEnd(e)); 525 526 // Keyboard navigation 527 this.$carousel.on('keydown', (e) => this.handleKeyboard(e)); 528 529 // Pause autoplay on hover 530 if (this.autoplay) { 531 this.$carousel.on('mouseenter', () => this.pauseAutoplay()); 532 this.$carousel.on('mouseleave', () => this.startAutoplay()); 533 } 534 535 // Pause autoplay when tab is not visible 536 document.addEventListener('visibilitychange', () => { 537 if (document.hidden) { 538 this.pauseAutoplay(); 539 } else if (this.autoplay) { 540 this.startAutoplay(); 541 } 542 }); 543 } 544 545 nextSlide() { 546 if (this.isCarouselMode && !this.isResponsiveSingleMode) { 547 // En modo carousel normal, mover por el número de slides visibles 548 const maxSlide = Math.max(0, this.totalSlides - this.visibleSlides);
549 if (this.currentSlide < maxSlide) { 550 this.goToSlide(Math.min(this.currentSlide + this.visibleSlides, maxSlide)); 551 } else { 552 this.goToSlide(0); // Loop to first slide 553 } 554 } else { 555 // En modo slider o modo responsivo single, mover de uno en uno 556 if (this.currentSlide < this.totalSlides - 1) { 557 this.goToSlide(this.currentSlide + 1); 558 } else { 559 this.goToSlide(0); // Loop to first slide 560 } 561 } 562 } 563 564 previousSlide() { 565 if (this.isCarouselMode && !this.isResponsiveSingleMode) { 566 // En modo carousel normal, mover por el número de slides visibles 567 if (this.currentSlide > 0) { 568 this.goToSlide(Math.max(0, this.currentSlide - this.visibleSlides)); 569 } else { 570 const maxSlide = Math.max(0, this.totalSlides - this.visibleSlides); 571 this.goToSlide(maxSlide); // Loop to last possible position 572 } 573 } else { 574 // En modo slider o modo responsivo single, mover de uno en uno 575 if (this.currentSlide > 0) { 576 this.goToSlide(this.currentSlide - 1); 577 } else { 578 this.goToSlide(this.totalSlides - 1); // Loop to last slide 579 } 580 } 581 } 582 583 goToSlide(slideIndex) { 584 if (this.isTransitioning) return; 585 586 if (this.isCarouselMode) { 587 // Check if we're in responsive single mode 588 if (this.isResponsiveSingleMode) { 589 // Behave like slider mode on small screens 590 if (slideIndex < 0 || slideIndex >= this.totalSlides) { 591 return; 592 } 593 594 this.isTransitioning = true; 595 this.currentSlide = slideIndex; 596 const translateX = -slideIndex * 100; 597 this.$track.css('transform', `translateX(${translateX}%)`); 598 } else { 599 // Normal carousel mode behavior 600 const maxSlide = Math.max(0, this.totalSlides - this.visibleSlides); 601 if (slideIndex < 0 || slideIndex > maxSlide) { 602 return; 603 } 604 605 this.isTransitioning = true; 606 this.currentSlide = slideIndex; 607 608 // En modo carousel, calcular el translateX basándose en el ancho de cada slide 609 const slideWidth = this.$slides.outerWidth(true); 610 if (slideWidth === 0) { 611 this.isTransitioning = false; 612 return; // Avoid moving if width is not ready 613 } 614 const translateX = -(slideIndex * slideWidth); 615 this.$track.css('transform', `translateX(${translateX}px)`); 616 } 617 } else { 618 // En modo slider, validación estándar 619 if (slideIndex < 0 || slideIndex >= this.totalSlides) { 620 return; 621 } 622 623 this.isTransitioning = true; 624 this.currentSlide = slideIndex; 625 626 // En modo slider, usar porcentajes 627 const translateX = -slideIndex * 100; 628 this.$track.css('transform', `translateX(${translateX}%)`); 629 } 630 631 // Reset the transitioning flag after the transition is complete 632 setTimeout(() => { 633 this.isTransitioning = false; 634 }, 300); // Match the transition duration in CSS 635 636 this.updateNavigation(); 637 638 // Trigger custom event 639 this.$carousel.trigger('slideChanged', [slideIndex]); 640 } 641 642 updateNavigation() { 643 // Update dots 644 this.$dots.removeClass('active'); 645 this.$dots.eq(this.currentSlide).addClass('active'); 646 647 // Update button states 648 this.$prevBtn.prop('disabled', false); 649 this.$nextBtn.prop('disabled', false); 650 651 // Note: We allow looping, so buttons are never disabled 652 } 653 654 startAutoplay() { 655 if (!this.autoplay || this.totalSlides <= 1) return; 656 657 this.pauseAutoplay(); // Clear any existing interval 658 this.autoplayInterval = setInterval(() => { 659 this.nextSlide(); 660 }, 5000); // 5 seconds default 661 } 662 663 pauseAutoplay() { 664 if (this.autoplayInterval) { 665 clearInterval(this.autoplayInterval); 666 this.autoplayInterval = null; 667 } 668 } 669 670 handleTouchStart(e) { 671 this.touchStartX = e.originalEvent.touches[0].clientX; 672 } 673 674 handleTouchEnd(e) { 675 this.touchEndX = e.originalEvent.changedTouches[0].clientX; 676 this.handleSwipe(); 677 } 678 679 handleSwipe() { 680 const swipeThreshold = 50; 681 const diff = this.touchStartX - this.touchEndX; 682 683 if (Math.abs(diff) > swipeThreshold) { 684 if (diff > 0) { 685 // Swiped left - next slide 686 this.nextSlide(); 687 } else { 688 // Swiped right - previous slide 689 this.previousSlide(); 690 } 691 } 692 } 693 694 handleKeyboard(e) { 695 switch (e.key) { 696 case 'ArrowLeft': 697 e.preventDefault(); 698 this.previousSlide(); 699 break; 700 case 'ArrowRight': 701 e.preventDefault(); 702 this.nextSlide(); 703 break; 704 case 'Home': 705 e.preventDefault(); 706 this.goToSlide(0); 707 break; 708 case 'End': 709 e.preventDefault(); 710 if (this.isCarouselMode) { 711 this.goToSlide(Math.max(0, this.totalSlides - this.visibleSlides)); 712 } else { 713 this.goToSlide(this.totalSlides - 1); 714 } 715 break; 716 } 717 } 718 719 // Calcular cuántos slides son visibles basándose en el ancho del contenedor 720 calculateVisibleSlides() { 721 const containerWidth = this.$carousel.width(); 722 const windowWidth = $(window).width(); 723 724 // Force single item on small screens (600px breakpoint) 725 if (windowWidth <= 600) { 726 this.visibleSlides = 1; 727 this.isResponsiveSingleMode = true; 728 return 1; 729 } 730 731 this.isResponsiveSingleMode = false;
732 const slideWidth = this.$slides.first().outerWidth(true); 733 734 if (slideWidth === 0 || containerWidth === 0) { 735 return 1; // Fallback si no se pueden calcular las dimensiones 736 } 737 738 this.visibleSlides = Math.floor(containerWidth / slideWidth); 739 return Math.max(1, this.visibleSlides); 740 } 741 742 setupCarouselMode() { 743 // Configurar estilos para el modo carousel (automático) 744 this.$slides.each(function() { 745 $(this).css({ 746 'flex': '0 0 auto', 747 'width': 'auto' 748 }); 749 }); 750 751 this.$track.css({ 752 'display': 'flex', 753 'transition': 'transform 0.3s ease-in-out' 754 }); 755 } 756 757 setupSliderMode() { 758 // Configurar estilos para el modo slider (1 artÃculo por vez) 759 this.$slides.each(function() { 760 $(this).css({ 761 'flex': '0 0 100%', 762 'width': '100%' 763 }); 764 }); 765 766 this.$track.css({ 767 'display': 'flex', 768 'transition': 'transform 0.3s ease-in-out' 769 }); 770 } 771 772 updateCarouselMode() { 773 if (!this.isCarouselMode) return; 774 775 // Recalcular slides visibles 776 this.calculateVisibleSlides(); 777 this.isTransitioning = false; // Allow transition after resize 778 779 // En modo responsivo single, resetear a slide actual o 0 780 if (this.isResponsiveSingleMode) { 781 // Ensure current slide is valid for single mode 782 if (this.currentSlide >= this.totalSlides) { 783 this.goToSlide(0); 784 } else { 785 this.goToSlide(this.currentSlide); 786 } 787 } else { 788 // Recalcular y ajustar la posición si es necesario para modo normal 789 const maxSlide = Math.max(0, this.totalSlides - this.visibleSlides); 790 if (this.currentSlide > maxSlide) { 791 this.goToSlide(maxSlide); 792 } else { 793 // Refresh current position 794 this.goToSlide(this.currentSlide); 795 } 796 } 797 } 798 } 799 800 // Intersection Observer for animations 801 const AnimationObserver = { 802 observer: null, 803 observedElements: new Set(), 804 805 init: function() { 806 if ('IntersectionObserver' in window && !this.observer) { 807 this.observer = new IntersectionObserver((entries) => { 808 entries.forEach(entry => { 809 if (entry.isIntersecting) { 810 entry.target.classList.add('rss-fade-in'); 811 this.observer.unobserve(entry.target); 812 this.observedElements.delete(entry.target); 813 } 814 }); 815 }, { 816 threshold: 0.1, 817 rootMargin: '0px 0px -50px 0px' 818 }); 819 } 820 821 this.observeNewElements(); 822 }, 823 824 observeNewElements: function() { 825 if (!this.observer) return; 826 827 document.querySelectorAll('.rss-feed-item:not(.rss-fade-in)').forEach(item => { 828 if (!this.observedElements.has(item)) { 829 this.observer.observe(item); 830 this.observedElements.add(item); 831 } 832 }); 833 } 834 }; 835 836 // Initialize when DOM is ready 837 $(document).ready(function() { 838 RSSFeedsDisplay.init(); 839 AnimationObserver.init(); 840 }); 841 842 // Re-initialize after AJAX content loads 843 $(document).ajaxComplete(function() { 844 AnimationObserver.observeNewElements(); 845 }); 846 847 // Expose to global scope for external access 848 window.RSSFeedsDisplay = RSSFeedsDisplay; 849 850})(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.