Jump to content
Wikipedia The Free Encyclopedia

User:Polygnotus/Scripts/Timeline.js

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.
Documentation for this user script can be added at User:Polygnotus/Scripts/Timeline.
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.
 //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();
 }
 })();

AltStyle によって変換されたページ (->オリジナル) /