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.

  1. 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:
  2. 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.
  3. 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.
  4. 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.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.