PageSourceSearch

https://mixfm.mx/wp-content/plugins/rss-feeds-display/assets/js/rss-feeds-display.js?ver=1.0.1

js mixfm.mx collected 2026-10-02 12:28:44 UTC 34,698 bytes, 850 lines download raw bytes

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.