User:Polygnotus/Scripts/DiscussionToolsDrafts.js
// <nowiki>
// Only run on the watchlist page
if (window.location.href.includes('wikipedia.org/wiki/Special:Watchlist')) {
// Main function to create and show the UI
function initDiscussionToolsManager() {
// Create main container with better styling
const container = document.createElement('div');
container.id = 'discussion-tools-manager';
container.style.margin = '20px 0';
container.style.padding = '15px';
container.style.border = '1px solid #a2a9b1';
container.style.borderRadius = '5px';
container.style.backgroundColor = '#f8f9fa';
container.style.fontFamily = 'sans-serif';
container.style.boxShadow = '0 1px 2px rgba(0,0,0,0.1)';
container.style.maxWidth = '100%';
// Add a header section
const header = document.createElement('div');
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.marginBottom = '15px';
header.style.borderBottom = '1px solid #eaecf0';
header.style.paddingBottom = '10px';
const titleLink = document.createElement('a');
titleLink.href = 'https://en.wikipedia.org/wiki/User:Polygnotus/Scripts/DiscussionToolsDrafts';
titleLink.textContent = 'DiscussionToolsDrafts';
titleLink.target = '_blank';
titleLink.style.backgroundImage = 'url(/w/skins/Vector/resources/skins.vector.styles.legacy/images/link-external-small-ltr-progressive.svg?fb64d)';
titleLink.style.backgroundPosition = 'center right';
titleLink.style.backgroundRepeat = 'no-repeat';
titleLink.style.backgroundSize = '0.857em';
titleLink.style.paddingRight = '1em';
header.appendChild(titleLink);
// Add buttons container
const buttonsContainer = document.createElement('div');
buttonsContainer.style.display = 'flex';
buttonsContainer.style.gap = '10px';
// Check if collapsed state is stored
const isCollapsed = localStorage.getItem('discussionToolsManagerCollapsed') === 'true';
// Add refresh button
const refreshButton = document.createElement('button');
refreshButton.className = 'cdx-button cdx-button--action-default';
refreshButton.textContent = '↻ Refresh';
refreshButton.addEventListener('click', function() {
refreshData();
});
buttonsContainer.appendChild(refreshButton);
// Add toggle button
const toggleButton = document.createElement('button');
toggleButton.className = 'cdx-button cdx-button--action-default';
toggleButton.textContent = isCollapsed ? '▼ Expand' : '▲ Collapse';
toggleButton.addEventListener('click', function() {
const contentArea = document.getElementById('discussion-tools-content-wrapper');
const isNowCollapsed = contentArea.style.display !== 'none';
// Toggle content area visibility
contentArea.style.display = isNowCollapsed ? 'none' : 'block';
toggleButton.textContent = isNowCollapsed ? '▼ Expand' : '▲ Collapse';
// Store preference in localStorage
localStorage.setItem('discussionToolsManagerCollapsed', isNowCollapsed.toString());
});
buttonsContainer.appendChild(toggleButton);
header.appendChild(buttonsContainer);
container.appendChild(header);
// Add description
const description = document.createElement('p');
description.innerHTML = 'This tool helps you manage saved DiscussionTools drafts in your browser storage. <span style="color: #3366cc; text-decoration: underline;">Click here to expand/collapse</span>.';
description.style.marginBottom = '15px';
description.style.color = '#54595d';
description.style.cursor = 'pointer';
// Add click event to description to toggle content area
description.addEventListener('click', function() {
const contentArea = document.getElementById('discussion-tools-content-wrapper');
const isNowCollapsed = contentArea.style.display !== 'none';
// Toggle content area visibility
contentArea.style.display = isNowCollapsed ? 'none' : 'block';
toggleButton.textContent = isNowCollapsed ? '▼ Expand' : '▲ Collapse';
// Store preference in localStorage
localStorage.setItem('discussionToolsManagerCollapsed', isNowCollapsed.toString());
});
container.appendChild(description);
// Create a wrapper for all content that can be collapsed
const contentWrapper = document.createElement('div');
contentWrapper.id = 'discussion-tools-content-wrapper';
// Set initial display state based on stored preference
contentWrapper.style.display = isCollapsed ? 'none' : 'block';
// Create delete button with updated text
const deleteButton = document.createElement('button');
deleteButton.className = 'cdx-button cdx-button--action-destructive';
deleteButton.textContent = 'Delete empty drafts';
deleteButton.style.marginBottom = '20px';
deleteButton.addEventListener('click', function() {
const deleted = deleteEmptyReplies();
alert(`Deleted ${deleted} empty or editsummary-only drafts.`);
refreshData();
});
contentWrapper.appendChild(deleteButton);
// Create content area that will be populated with data
const contentArea = document.createElement('div');
contentArea.id = 'discussion-tools-content';
contentWrapper.appendChild(contentArea);
// Add the wrapper to the container
container.appendChild(contentWrapper);
// Add the container below the main content div
const contentDiv = document.querySelector('div#content');
if (contentDiv) {
contentDiv.parentNode.insertBefore(container, contentDiv.nextSibling);
} else {
document.body.appendChild(container);
}
// Add animation styles
const style = document.createElement('style');
style.textContent = `
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.notification {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #28a745;
color: white;
padding: 10px 15px;
border-radius: 4px;
z-index: 9999;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
transition: opacity 0.5s;
}
`;
document.head.appendChild(style);
// Load initial data
refreshData();
}
// Function to refresh the data display
function refreshData() {
const contentArea = document.getElementById('discussion-tools-content');
if (!contentArea) return; // Safety check
contentArea.innerHTML = ''; // Clear existing content
const results = findDiscussionToolsReplyPairs();
if (Object.keys(results).length === 0) {
const noResults = document.createElement('p');
noResults.textContent = 'No DiscussionTools reply drafts were found in your browser storage.';
noResults.style.padding = '10px';
noResults.style.backgroundColor = '#eaecf0';
noResults.style.borderRadius = '4px';
contentArea.appendChild(noResults);
return;
}
// Create section for localStorage
const section = createDraftsSection('Drafts', results);
contentArea.appendChild(section);
}
// Function to create a drafts section
function createDraftsSection(title, entries) {
const section = document.createElement('div');
section.className = 'storage-section';
section.style.marginBottom = '15px';
section.style.border = '1px solid #c8ccd1';
section.style.borderRadius = '4px';
section.style.overflow = 'hidden';
// Create header
const header = document.createElement('div');
header.className = 'section-header';
header.style.padding = '10px 15px';
header.style.backgroundColor = '#eaecf0';
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
// Add title and count
const sectionTitle = document.createElement('span');
sectionTitle.innerHTML = `<strong>${title}</strong> <span style="color:#54595d">(${Object.keys(entries).length} entries)</span>`;
header.appendChild(sectionTitle);
section.appendChild(header);
// Create content area
const content = document.createElement('div');
content.className = 'section-content';
content.style.maxHeight = '500px';
content.style.overflow = 'auto';
// Populate with entries
const list = document.createElement('ul');
list.style.listStyleType = 'none';
list.style.padding = '0';
list.style.margin = '0';
for (const [key, value] of Object.entries(entries)) {
// Format the item
const item = document.createElement('li');
item.style.padding = '12px 15px';
item.style.borderBottom = '1px solid #eaecf0';
item.style.display = 'flex';
item.style.justifyContent = 'space-between';
item.style.alignItems = 'flex-start';
// Highlight empty or summary-only replies
if (isEmptyOrSummaryOnlyReply(value)) {
item.style.backgroundColor = '#ffeaea';
}
// Create the content container (left side)
const contentDiv = document.createElement('div');
contentDiv.style.flexGrow = '1';
contentDiv.style.paddingRight = '10px';
// Format page title from key and create actual links
let formattedKey = key;
try {
// Check if the key is in the format "mw-ext-DiscussionTools-reply/c-XXXXXXXX"
const isCommentID = key.includes('/c-');
if (isCommentID) {
// Extract the comment ID
const commentParts = key.split('/');
const prefix = commentParts[0]; // "mw-ext-DiscussionTools-reply"
const commentId = commentParts[1]; // c-XXXXXXXX
// Create the Special:GoToComment link
const commentUrl = `/wiki/Special:GoToComment/${commentId}`;
// Format with actual link to the comment
formattedKey = `<span style="color:#54595d">${prefix}</span> / <a href="${commentUrl}" style="color:#3366cc" title="Go to this specific comment">${commentId}</a>`;
}
// For page-based keys in the format "mw-ext-DiscussionTools-reply|PageName"
else if (key.includes('|')) {
const parts = key.split('|');
const prefix = parts[0]; // The prefix part (like "mw-ext-DiscussionTools-reply")
const pageName = parts[1].replace(/_/g, ' '); // The page name
// Create the wiki URL
const pageUrl = `/wiki/${parts[1]}`; // Use the raw page name with underscores for the URL
// Format with actual link
formattedKey = `<span style="color:#54595d">${prefix}</span> | <a href="${pageUrl}" style="color:#3366cc">${pageName}</a>`;
}
} catch (e) {
// If there's an error in parsing, use the original key
console.log('Error parsing key:', e);
}
contentDiv.innerHTML = `<div><strong>${formattedKey}</strong></div><div style="font-family:monospace;margin-top:5px;word-break:break-all;color:#54595d">${value}</div>`;
item.appendChild(contentDiv);
// Create delete button for individual entry
const deleteEntryBtn = document.createElement('button');
deleteEntryBtn.className = 'cdx-button cdx-button--action-destructive cdx-button--icon-only';
deleteEntryBtn.setAttribute('aria-label', 'Delete entry');
//deleteEntryBtn.textContent = 'Delete';
deleteEntryBtn.innerHTML = '<span class="cdx-icon cdx-icon--medium"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>delete</title><g><path d="M17 2h-3.5l-1-1h-5l-1 1H3v2h14zM4 17a2 2 0 002 2h8a2 2 0 002-2V5H4z"></path></g></svg></span>';
//const trashIcon = document.createElement('span');
//trashIcon.className = 'cdx-button__icon cdx-button__icon--trash';
//deleteEntryBtn.appendChild(trashIcon);
// Add delete functionality
deleteEntryBtn.addEventListener('click', function(e) {
e.stopPropagation(); // Prevent triggering parent click events
localStorage.removeItem(key);
// Remove the item from the display
item.style.animation = 'fadeOut 0.3s';
setTimeout(() => {
item.remove();
// Update the count in the section header
const countSpan = section.querySelector('.section-header span span');
if (countSpan) {
const currentCount = parseInt(countSpan.textContent.match(/\d+/)[0]);
countSpan.textContent = `(${currentCount - 1} entries)`;
}
}, 300);
// Show success message
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = `Deleted entry: ${key.split('|')[1] || key.split('/')[1] || key}`;
document.body.appendChild(notification);
// Remove notification after 3 seconds
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => notification.remove(), 500);
}, 3000);
});
item.appendChild(deleteEntryBtn);
list.appendChild(item);
}
content.appendChild(list);
section.appendChild(content);
// Always display the content
content.style.display = 'block';
return section;
}
// Function to find all key-value pairs where the key begins with "mw-ext-DiscussionTools-reply"
function findDiscussionToolsReplyPairs() {
const targetPrefix = "mw-ext-DiscussionTools-reply";
const result = {};
// Search in localStorage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(targetPrefix)) {
result[key] = localStorage.getItem(key);
}
}
return result;
}
// Function to check if a reply is empty or only contains a summary without real content
function isEmptyOrSummaryOnlyReply(value) {
try {
const parsed = JSON.parse(value);
// Check for completely empty replies: {"title":""}
if (parsed && typeof parsed === 'object' &&
Object.keys(parsed).length === 1 &&
'title' in parsed &&
parsed.title === '') {
return true;
}
// Check for empty replies with advanced options: {"showAdvanced":"","title":"","saveable":"","mode":"source"}
if (parsed && typeof parsed === 'object' &&
'title' in parsed && parsed.title === '' &&
'showAdvanced' in parsed && parsed.showAdvanced === '' &&
'mode' in parsed &&
(!('ve-changes' in parsed) || !parsed['ve-changes'] || parsed['ve-changes'].length === 0)) {
return true;
}
// Check for replies that only have a summary but no actual content
// This catches cases like: {"showAdvanced":"","saveable":"","mode":"source","summary":"/* Some section */ Reply"}
if (parsed && typeof parsed === 'object' &&
'summary' in parsed &&
'mode' in parsed &&
(!('ve-changes' in parsed) || !parsed['ve-changes'] || parsed['ve-changes'].length === 0)) {
return true;
}
// Additional check for replies with ve-changes but no actual content entered
if (parsed && typeof parsed === 'object' &&
've-changes' in parsed &&
Array.isArray(parsed['ve-changes']) &&
parsed['ve-changes'].length === 0) {
return true;
}
return false;
} catch (e) {
// If we can't parse it, assume it's not empty
return false;
}
}
// Function to delete empty replies
function deleteEmptyReplies() {
const targetPrefix = "mw-ext-DiscussionTools-reply";
let deletedCount = 0;
// Check localStorage
const localStorageKeys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(targetPrefix)) {
localStorageKeys.push(key);
}
}
for (const key of localStorageKeys) {
const value = localStorage.getItem(key);
if (isEmptyOrSummaryOnlyReply(value)) {
localStorage.removeItem(key);
deletedCount++;
console.log(`Deleted from localStorage: ${key}`);
}
}
return deletedCount;
}
// Initialize the tool
initDiscussionToolsManager();
}
// </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.