User:Polygnotus/Scripts/Timeline.js
Appearance
From Wikipedia, the free encyclopedia
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This code will be executed when previewing this page.
Documentation for this user script can be added at User:Polygnotus/Scripts/Timeline.
//This adds a timelinescrollbar allowing you to scroll through time through a discussion (if it has 10+ comments) //Status: unfinished, proof of concept (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 functionextractCommentsFromSection(section){ constcomments=[]; // Determine the level of this section constsectionLevel=section.className.match(/mw-heading(\d)/)?.[1]||'2'; // Find all comment markers in this section letcurrentElement=section.nextElementSibling; while(currentElement){ // Only stop if we hit a section of the same or higher level constheadingMatch=currentElement.className?.match(/mw-heading(\d)/); if(headingMatch){ constcurrentLevel=headingMatch[1]; if(parseInt(currentLevel)<=parseInt(sectionLevel)){ break; } } // Find all comment start markers constcommentStarts=currentElement.querySelectorAll('[data-mw-comment-start]'); commentStarts.forEach(startMarker=>{ // Find the complete comment by looking for all elements between start and the signature constcommentElements=[]; letsearchElement=startMarker.closest('p, dd, div'); if(!searchElement){ searchElement=startMarker.parentElement; } // Keep collecting elements until we find the timestamp/signature letfoundTimestamp=false; lettempElement=searchElement; letauthor='Unknown'; lettimestampLink=null; lettimestampText=''; while(tempElement&&!foundTimestamp){ commentElements.push(tempElement); // Check if this element contains the timestamp consttimestamp=tempElement.querySelector('.ext-discussiontools-init-timestamplink'); if(timestamp){ timestampLink=timestamp; timestampText=timestamp.textContent; foundTimestamp=true; // Look for author in the same element constauthorLink=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 consttimestampMatch=timestampText.match(/(\d{1,2}):(\d{2}), (\d{1,2}) (\w+) (\d{4})/); if(timestampMatch){ const[,hours,minutes,day,month,year]=timestampMatch; constmonthNames=['January','February','March','April','May','June', 'July','August','September','October','November','December']; constmonthIndex=monthNames.indexOf(month); consttimestamp=newDate(year,monthIndex,day,hours,minutes); comments.push({ id:startMarker.id, elements:commentElements, author:author, timestamp:timestamp, timestampText:timestampText }); } } }); currentElement=currentElement.nextElementSibling; } returncomments.sort((a,b)=>a.timestamp-b.timestamp); } // Create timeline slider UI for a specific section functioncreateTimelineSlider(section){ constcomments=extractCommentsFromSection(section); if(comments.length<10)return;// Only add slider for sections with 10+ comments // Store original display values to restore them properly constoriginalDisplayValues=newMap(); comments.forEach(comment=>{ comment.elements.forEach(element=>{ originalDisplayValues.set(element,element.style.display||''); }); }); constsliderContainer=document.createElement('div'); sliderContainer.style.cssText=` margin: 10px 0; padding: 10px; background-color: #f8f9fa; border: 1px solid #a2a9b1; border-radius: 2px; `; constsliderLabel=document.createElement('div'); sliderLabel.style.cssText=` font-weight: bold; margin-bottom: 5px; `; sliderLabel.textContent='Timeline: '; constdateDisplay=document.createElement('span'); dateDisplay.style.cssText=` font-weight: normal; margin-left: 5px; `; sliderLabel.appendChild(dateDisplay); // Comment counter display constcommentCounter=document.createElement('div'); commentCounter.style.cssText=` font-size: 0.9em; color: #555; margin-bottom: 5px; `; constslider=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 constbuttonContainer=document.createElement('div'); buttonContainer.style.cssText=` display: flex; gap: 10px; align-items: center; margin-top: 10px; `; constprevButton=document.createElement('button'); prevButton.textContent='← Previous'; prevButton.style.cssText=` padding: 5px 10px; background-color: #36c; color: white; border: none; border-radius: 2px; cursor: pointer; `; constnextButton=document.createElement('button'); nextButton.textContent='Next →'; nextButton.style.cssText=` padding: 5px 10px; background-color: #36c; color: white; border: none; border-radius: 2px; cursor: pointer; `; constresetButton=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 letisInitialLoad=true; letupdateInProgress=false; letpendingUpdate=null; // Function to update visible comments for this section only functionupdateVisibleComments(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 constisDarkMode=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 constallReplies=[]; letcurrentElement=section.nextElementSibling; constnextSectionLevel=section.className.match(/mw-heading(\d)/)?.[1]||'2'; while(currentElement){ // Stop if we hit a section of the same or higher level constheadingMatch=currentElement.className?.match(/mw-heading(\d)/); if(headingMatch){ constcurrentLevel=headingMatch[1]; if(parseInt(currentLevel)<=parseInt(nextSectionLevel)){ break; } } // Find replies constreplies=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'; constreplyTimestamp=reply.querySelector('.ext-discussiontools-init-timestamplink'); if(replyTimestamp){ // Parse reply timestamp constreplyTimeText=replyTimestamp.textContent; constreplyMatch=replyTimeText.match(/(\d{1,2}):(\d{2}), (\d{1,2}) (\w+) (\d{4})/); if(replyMatch){ const[,hours,minutes,day,month,year]=replyMatch; constmonthNames=['January','February','March','April','May','June', 'July','August','September','October','November','December']; constmonthIndex=monthNames.indexOf(month); constreplyTime=newDate(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){ constnextUpdate=pendingUpdate; pendingUpdate=null; updateVisibleComments(nextUpdate); } } } // Debounced update function for slider input letsliderTimeout; functiondebouncedUpdate(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',()=>{ constcurrentValue=parseInt(slider.value); if(currentValue>0){ slider.value=currentValue-1; updateVisibleComments(currentValue-1); } }); nextButton.addEventListener('click',()=>{ constcurrentValue=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 constsectionBar=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 functionaddTimelineSliders(){ constsections=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(); } })();