User:Polygnotus/Scripts/Timeline.js
// <nowiki>
//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;
}
// Get the boundary of a section - returns the next sibling element that starts a new section
function getSectionBoundary(section) {
const sectionLevel = section.className.match(/mw-heading(\d)/)?.[1] || '2';
let currentElement = section.nextElementSibling;
while (currentElement) {
const headingMatch = currentElement.className?.match(/mw-heading(\d)/);
if (headingMatch) {
const currentLevel = headingMatch[1];
if (parseInt(currentLevel) <= parseInt(sectionLevel)) {
return currentElement;
}
}
currentElement = currentElement.nextElementSibling;
}
return null; // No next section found
}
// Extract comment data from a specific section, including all reply elements
function extractCommentsFromSection(section) {
const comments = [];
const boundary = getSectionBoundary(section);
const allTimestampedElements = [];
// First pass: collect all timestamped elements within this section
let currentElement = section.nextElementSibling;
while (currentElement && currentElement !== boundary) {
const timestamps = currentElement.querySelectorAll('.ext-discussiontools-init-timestamplink');
timestamps.forEach(timestamp => {
const element = timestamp.closest('p, dd, div, li');
if (element) {
const timestampText = timestamp.textContent;
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 timestampDate = new Date(year, monthIndex, day, hours, minutes);
const authorLink = element.querySelector('a[href*="User:"]');
const author = authorLink ? authorLink.textContent : 'Unknown';
const commentStart = element.querySelector('[data-mw-comment-start]');
const commentId = commentStart ? commentStart.id : `comment-${Date.now()}-${Math.random()}`;
allTimestampedElements.push({
id: commentId,
element: element,
author: author,
timestamp: timestampDate,
timestampText: timestampText
});
}
}
});
currentElement = currentElement.nextElementSibling;
}
// Sort by timestamp
allTimestampedElements.sort((a, b) => a.timestamp - b.timestamp);
// Group into comments with their associated elements
allTimestampedElements.forEach(item => {
comments.push({
id: item.id,
elements: [item.element],
author: item.author,
timestamp: item.timestamp,
timestampText: item.timestampText
});
});
return comments;
}
// Create timeline slider UI for a specific section
function createTimelineSlider(section) {
const comments = extractCommentsFromSection(section);
if (comments.length < 10) return;
// Store original display values
const originalDisplayValues = new Map();
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);
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;
slider.style.cssText = `
width: 100%;
margin: 10px 0;
`;
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;
`;
let isInitialLoad = true;
function updateVisibleComments(index, shouldScroll = true) {
index = Math.max(0, Math.min(index, comments.length - 1));
dateDisplay.textContent = `${comments[index].timestampText} by ${comments[index].author}`;
commentCounter.textContent = `Showing comments 1-${index + 1} of ${comments.length}`;
prevButton.disabled = index === 0;
nextButton.disabled = index === comments.length - 1;
// Update comment visibility - only touch elements that belong to this section's comments
comments.forEach((comment, i) => {
comment.elements.forEach(element => {
if (!element.parentNode) return;
// Store original display value if not already stored
if (!originalDisplayValues.has(element)) {
originalDisplayValues.set(element, element.style.display || '');
}
if (i <= index) {
element.style.display = originalDisplayValues.get(element) || '';
if (i === index) {
const isDarkMode = document.body.classList.contains('dark-mode') ||
document.body.classList.contains('skin-theme-clientpref-night') ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
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 {
element.style.display = 'none';
}
});
});
if (shouldScroll && !isInitialLoad && index > 0 && comments[index].elements[0] && comments[index].elements[0].parentNode) {
comments[index].elements[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
if (isInitialLoad) {
isInitialLoad = false;
}
}
// Throttle function to limit update frequency during dragging
let throttleTimeout = null;
let lastUpdateTime = 0;
const throttleDelay = 50; // Update at most every 50ms
function throttledUpdate(index) {
const now = Date.now();
const timeSinceLastUpdate = now - lastUpdateTime;
if (timeSinceLastUpdate >= throttleDelay) {
// Enough time has passed, update immediately
lastUpdateTime = now;
updateVisibleComments(index, false); // Don't scroll during drag
} else {
// Schedule an update for later
clearTimeout(throttleTimeout);
throttleTimeout = setTimeout(() => {
lastUpdateTime = Date.now();
updateVisibleComments(index, false);
}, throttleDelay - timeSinceLastUpdate);
}
}
// Handle slider input - update continuously while dragging
slider.addEventListener('input', (e) => {
throttledUpdate(parseInt(e.target.value));
});
// Handle slider change - final update when released
slider.addEventListener('change', (e) => {
clearTimeout(throttleTimeout);
updateVisibleComments(parseInt(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);
});
sliderContainer.appendChild(sliderLabel);
sliderContainer.appendChild(commentCounter);
sliderContainer.appendChild(slider);
buttonContainer.appendChild(prevButton);
buttonContainer.appendChild(nextButton);
buttonContainer.appendChild(resetButton);
sliderContainer.appendChild(buttonContainer);
sliderContainer.classList.add('timeline-slider');
const sectionBar = section.querySelector('.ext-discussiontools-init-section-bar');
if (sectionBar) {
sectionBar.insertAdjacentElement('afterend', sliderContainer);
} else {
section.insertAdjacentElement('afterbegin', sliderContainer);
}
updateVisibleComments(comments.length - 1, false);
}
function addTimelineSliders() {
const sections = document.querySelectorAll('div.mw-heading2');
sections.forEach(section => {
if (section.querySelector('h2') && !section.querySelector('.timeline-slider')) {
createTimelineSlider(section);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', addTimelineSliders);
} else {
addTimelineSliders();
}
})();
// </nowiki>
Content Disclaimer
Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.
- The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
- There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
- It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
- Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.