Jump to content

User:Polygnotus/Scripts/Timeline.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
(function() {
    'use strict';
    
    // Only run on discussion pages
    if (!mw.config.get('wgIsArticle') || !document.querySelector('.ext-discussiontools-init-section')) {
        return;
    }
    
    // Extract comment data from a specific section
    function extractCommentsFromSection(section) {
        const comments = [];
        
        // Determine the level of this section
        const sectionLevel = section.className.match(/mw-heading(\d)/)?.[1] || '2';
        
        // Find all comment markers in this section
        let currentElement = section.nextElementSibling;
        
        while (currentElement) {
            // Only stop if we hit a section of the same or higher level
            const headingMatch = currentElement.className?.match(/mw-heading(\d)/);
            if (headingMatch) {
                const currentLevel = headingMatch[1];
                if (parseInt(currentLevel) <= parseInt(sectionLevel)) {
                    break;
                }
            }
            
            // Find all comment start markers
            const commentStarts = currentElement.querySelectorAll('[data-mw-comment-start]');
            
            commentStarts.forEach(startMarker => {
                // Find the complete comment by looking for all elements between start and the signature
                const commentElements = [];
                let searchElement = startMarker.closest('p, dd, div');
                
                if (!searchElement) {
                    searchElement = startMarker.parentElement;
                }
                
                // Keep collecting elements until we find the timestamp/signature
                let foundTimestamp = false;
                let tempElement = searchElement;
                let author = 'Unknown';
                let timestampLink = null;
                let timestampText = '';
                
                while (tempElement && !foundTimestamp) {
                    commentElements.push(tempElement);
                    
                    // Check if this element contains the timestamp
                    const timestamp = tempElement.querySelector('.ext-discussiontools-init-timestamplink');
                    if (timestamp) {
                        timestampLink = timestamp;
                        timestampText = timestamp.textContent;
                        foundTimestamp = true;
                        
                        // Look for author in the same element
                        const authorLink = tempElement.querySelector('a[href*="User:"]');
                        if (authorLink) {
                            author = authorLink.textContent;
                        }
                    }
                    
                    // Move to next sibling
                    tempElement = tempElement.nextElementSibling;
                }
                
                // Only create a comment if we found both start and timestamp
                if (timestampLink && commentElements.length > 0) {
                    // Parse timestamp
                    const timestampMatch = timestampText.match(/(\d{1,2}):(\d{2}), (\d{1,2}) (\w+) (\d{4})/);
                    if (timestampMatch) {
                        const [, hours, minutes, day, month, year] = timestampMatch;
                        const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 
                                           'July', 'August', 'September', 'October', 'November', 'December'];
                        const monthIndex = monthNames.indexOf(month);
                        const timestamp = new Date(year, monthIndex, day, hours, minutes);
                        
                        comments.push({
                            id: startMarker.id,
                            elements: commentElements,
                            author: author,
                            timestamp: timestamp,
                            timestampText: timestampText
                        });
                    }
                }
            });
            
            currentElement = currentElement.nextElementSibling;
        }
        
        return comments.sort((a, b) => a.timestamp - b.timestamp);
    }
    
    // Create timeline slider UI for a specific section
    function createTimelineSlider(section) {
        const comments = extractCommentsFromSection(section);
        if (comments.length < 10) return; // Only add slider for sections with 10+ comments
        
        // Store original display values to restore them properly
        const originalDisplayValues = new Map();
        comments.forEach(comment => {
            comment.elements.forEach(element => {
                originalDisplayValues.set(element, element.style.display || '');
            });
        });
        
        const sliderContainer = document.createElement('div');
        sliderContainer.style.cssText = `
            margin: 10px 0;
            padding: 10px;
            background-color: #f8f9fa;
            border: 1px solid #a2a9b1;
            border-radius: 2px;
        `;
        
        const sliderLabel = document.createElement('div');
        sliderLabel.style.cssText = `
            font-weight: bold;
            margin-bottom: 5px;
        `;
        sliderLabel.textContent = 'Timeline: ';
        
        const dateDisplay = document.createElement('span');
        dateDisplay.style.cssText = `
            font-weight: normal;
            margin-left: 5px;
        `;
        sliderLabel.appendChild(dateDisplay);
        
        // Comment counter display
        const commentCounter = document.createElement('div');
        commentCounter.style.cssText = `
            font-size: 0.9em;
            color: #555;
            margin-bottom: 5px;
        `;
        
        const slider = document.createElement('input');
        slider.type = 'range';
        slider.min = '0';
        slider.max = comments.length - 1;
        slider.value = comments.length - 1; // Default to showing all comments
        slider.style.cssText = `
            width: 100%;
            margin: 10px 0;
        `;
        
        // Step forward/backward buttons
        const buttonContainer = document.createElement('div');
        buttonContainer.style.cssText = `
            display: flex;
            gap: 10px;
            align-items: center;
            margin-top: 10px;
        `;
        
        const prevButton = document.createElement('button');
        prevButton.textContent = '← Previous';
        prevButton.style.cssText = `
            padding: 5px 10px;
            background-color: #36c;
            color: white;
            border: none;
            border-radius: 2px;
            cursor: pointer;
        `;
        
        const nextButton = document.createElement('button');
        nextButton.textContent = 'Next →';
        nextButton.style.cssText = `
            padding: 5px 10px;
            background-color: #36c;
            color: white;
            border: none;
            border-radius: 2px;
            cursor: pointer;
        `;
        
        const resetButton = document.createElement('button');
        resetButton.textContent = 'Show All';
        resetButton.style.cssText = `
            padding: 5px 10px;
            background-color: #36c;
            color: white;
            border: none;
            border-radius: 2px;
            cursor: pointer;
            margin-left: auto;
        `;
        
        // Flag to prevent scrolling on initial load
        let isInitialLoad = true;
        let updateInProgress = false;
        let pendingUpdate = null;
        
        // Function to update visible comments for this section only
        function updateVisibleComments(index) {
            // Prevent concurrent updates
            if (updateInProgress) {
                pendingUpdate = index;
                return;
            }
            
            updateInProgress = true;
            
            try {
                // Validate index
                index = Math.max(0, Math.min(index, comments.length - 1));
                
                // Update displays
                dateDisplay.textContent = `${comments[index].timestampText} by ${comments[index].author}`;
                commentCounter.textContent = `Showing comments 1-${index + 1} of ${comments.length}`;
                
                // Update button states
                prevButton.disabled = index === 0;
                nextButton.disabled = index === comments.length - 1;
                
                // Show/hide comments based on index
                comments.forEach((comment, i) => {
                    comment.elements.forEach(element => {
                        // Make sure element is still in the DOM
                        if (!element.parentNode) {
                            return;
                        }
                        
                        if (i <= index) {
                            // Restore original display value
                            element.style.display = originalDisplayValues.get(element) || '';
                            
                            // Highlight the most recent comment (current index)
                            if (i === index) {
                                // Check for dark mode
                                const isDarkMode = document.body.classList.contains('dark-mode') || 
                                                document.body.classList.contains('skin-theme-clientpref-night') ||
                                                window.matchMedia('(prefers-color-scheme: dark)').matches;
                                
                                // Set appropriate background color
                                element.style.backgroundColor = isDarkMode ? 'rgba(255, 255, 204, 0.1)' : 'rgba(255, 255, 204, 0.5)';
                                element.style.transition = 'background-color 0.3s ease';
                            } else {
                                element.style.backgroundColor = '';
                            }
                        } else {
                            // Hide this element
                            element.style.display = 'none';
                        }
                    });
                });
                
                // Handle all replies in the section
                const allReplies = [];
                let currentElement = section.nextElementSibling;
                const nextSectionLevel = section.className.match(/mw-heading(\d)/)?.[1] || '2';
                
                while (currentElement) {
                    // Stop if we hit a section of the same or higher level
                    const headingMatch = currentElement.className?.match(/mw-heading(\d)/);
                    if (headingMatch) {
                        const currentLevel = headingMatch[1];
                        if (parseInt(currentLevel) <= parseInt(nextSectionLevel)) {
                            break;
                        }
                    }
                    
                    // Find replies
                    const replies = currentElement.querySelectorAll('dd:not([data-mw-comment-start]), dl dl');
                    replies.forEach(reply => {
                        // Only include replies that aren't themselves comment starts
                        if (!reply.querySelector('[data-mw-comment-start]')) {
                            allReplies.push(reply);
                            // Store original display value if not already stored
                            if (!originalDisplayValues.has(reply)) {
                                originalDisplayValues.set(reply, reply.style.display || '');
                            }
                        }
                    });
                    
                    currentElement = currentElement.nextElementSibling;
                }
                
                allReplies.forEach(reply => {
                    // Make sure reply is still in the DOM
                    if (!reply.parentNode) {
                        return;
                    }
                    
                    // Hide all replies by default, then show only those that should be visible
                    reply.style.display = 'none';
                    
                    const replyTimestamp = reply.querySelector('.ext-discussiontools-init-timestamplink');
                    if (replyTimestamp) {
                        // Parse reply timestamp
                        const replyTimeText = replyTimestamp.textContent;
                        const replyMatch = replyTimeText.match(/(\d{1,2}):(\d{2}), (\d{1,2}) (\w+) (\d{4})/);
                        if (replyMatch) {
                            const [, hours, minutes, day, month, year] = replyMatch;
                            const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 
                                               'July', 'August', 'September', 'October', 'November', 'December'];
                            const monthIndex = monthNames.indexOf(month);
                            const replyTime = new Date(year, monthIndex, day, hours, minutes);
                            
                            // Only show replies that are older than or equal to the current comment's timestamp
                            if (replyTime <= comments[index].timestamp) {
                                reply.style.display = originalDisplayValues.get(reply) || '';
                            }
                        }
                    }
                });
                
                // Scroll to the current comment only if not initial load and not the first comment
                if (!isInitialLoad && index > 0 && comments[index].elements[0].parentNode) {
                    comments[index].elements[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
                }
                
                // Clear the initial load flag after first run
                if (isInitialLoad) {
                    isInitialLoad = false;
                }
            } finally {
                updateInProgress = false;
                
                // Process any pending update
                if (pendingUpdate !== null) {
                    const nextUpdate = pendingUpdate;
                    pendingUpdate = null;
                    updateVisibleComments(nextUpdate);
                }
            }
        }
        
        // Debounced update function for slider input
        let sliderTimeout;
        function debouncedUpdate(value) {
            clearTimeout(sliderTimeout);
            sliderTimeout = setTimeout(() => {
                updateVisibleComments(parseInt(value));
            }, 50); // Small delay to prevent rapid updates
        }
        
        // Event handlers - scoped to this section's slider
        slider.addEventListener('input', (e) => {
            debouncedUpdate(e.target.value);
        });
        
        prevButton.addEventListener('click', () => {
            const currentValue = parseInt(slider.value);
            if (currentValue > 0) {
                slider.value = currentValue - 1;
                updateVisibleComments(currentValue - 1);
            }
        });
        
        nextButton.addEventListener('click', () => {
            const currentValue = parseInt(slider.value);
            if (currentValue < comments.length - 1) {
                slider.value = currentValue + 1;
                updateVisibleComments(currentValue + 1);
            }
        });
        
        resetButton.addEventListener('click', () => {
            slider.value = comments.length - 1;
            updateVisibleComments(comments.length - 1);
        });
        
        // Assemble the UI
        sliderContainer.appendChild(sliderLabel);
        sliderContainer.appendChild(commentCounter);
        sliderContainer.appendChild(slider);
        buttonContainer.appendChild(prevButton);
        buttonContainer.appendChild(nextButton);
        buttonContainer.appendChild(resetButton);
        sliderContainer.appendChild(buttonContainer);
        
        // Add class for styling reference
        sliderContainer.classList.add('timeline-slider');
        
        // Insert after the section header
        const sectionBar = section.querySelector('.ext-discussiontools-init-section-bar');
        if (sectionBar) {
            sectionBar.insertAdjacentElement('afterend', sliderContainer);
        } else {
            section.insertAdjacentElement('afterbegin', sliderContainer);
        }
        
        // Initialize with all comments visible
        updateVisibleComments(comments.length - 1);
    }
    
    // Add timeline sliders to all discussion sections
    function addTimelineSliders() {
        const sections = document.querySelectorAll('div.mw-heading2');
        sections.forEach(section => {
            if (section.querySelector('h2') && !section.querySelector('.timeline-slider')) {
                createTimelineSlider(section);
            }
        });
    }
    
    // Wait for page to load
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', addTimelineSliders);
    } else {
        addTimelineSliders();
    }
})();