User:Polygnotus/Scripts/CategoryToClipboard.js: Difference between revisions

Content deleted Content added
Created page with '// Wikipedia Category Items Copier // This script adds two buttons to Wikipedia category pages: // 1. "Copy Items" - Copies all items in the current category // 2. "Copy All Items" - Copies all items in the current category and its subcategories // Check if we're on a Wikipedia category page if (!window.___location.href.includes('/wiki/Category:')) { alert('This script only works on Wikipedia category pages.'); } else { // Create a container for our b...'
 
add "both" and "both recursive" buttons, re-order
 
(23 intermediate revisions by the same user not shown)
Line 1:
// Wikipedia Category Items Copier - Fixed Redirect Filtering
// This script adds two buttons to Wikipedia category pages:
// 1. "Copy Items" - Copies all items in the current category
// 2. "Copy All Items" - Copies all items in the current category and its subcategories
 
const API_DELAY = 500; // Delay between API requests in milliseconds
// Check if we're on a Wikipedia category page
const MAX_RETRIES = 3; // Maximum number of retries for failed requests
if (!window.___location.href.includes('/wiki/Category:')) {
 
alert('This script only works on Wikipedia category pages.');
// Only run on Wikipedia category pages
} else {
if (window.___location.href.includes('/wiki/Category:')) {
// Extract the category name from the URL
const categoryName = decodeURIComponent(window.___location.pathname.split('/Category:')[1]);
console.log("Category Name:", categoryName);
// Create a container for our buttons
const container = document.createElement('div');
Line 15 ⟶ 17:
container.style.border = '1px solid #a2a9b1';
container.style.borderRadius = '3px';
 
// Helper function to create tooltip
function addTooltip(element, text) {
element.title = text;
element.style.position = 'relative';
}
 
// Create the "Copy Items" button
const copyItemsBtn = document.createElement('button');
copyItemsBtn.textContent = 'Copy Items from this Categoryitems';
copyItemsBtn.style.marginRight = '10px';
copyItemsBtn.style.padding = '8px 12px';
copyItemsBtn.style.cursor = 'pointer';
addTooltip(copyItemsBtn, 'Copy all items in this category. Not recursive.');
// Create the "Copy All Items" button
const copyAllItemsBtn = document.createElement('button');
copyAllItemsBtn.textContent = 'Copy Itemsitems from All Subcategoriesrecursively';
copyAllItemsBtn.style.marginRight = '10px';
copyAllItemsBtn.style.padding = '8px 12px';
copyAllItemsBtn.style.cursor = 'pointer';
addTooltip(copyAllItemsBtn, 'Copy all items in this category AND all items in its subcategories.');
// Create the "Copy Subcats from this Category" button
const copyDirectSubcatsBtn = document.createElement('button');
copyDirectSubcatsBtn.textContent = 'Copy subcats';
copyDirectSubcatsBtn.style.marginRight = '10px';
copyDirectSubcatsBtn.style.padding = '8px 12px';
copyDirectSubcatsBtn.style.cursor = 'pointer';
addTooltip(copyDirectSubcatsBtn, 'Copy all subcategories of this category. Not recursive.');
// Create the "Copy Subcategories" button
const copySubcatsBtn = document.createElement('button');
copySubcatsBtn.textContent = 'Copy subcategories recursively';
copySubcatsBtn.style.marginRight = '10px';
copySubcatsBtn.style.padding = '8px 12px';
copySubcatsBtn.style.cursor = 'pointer';
addTooltip(copySubcatsBtn, 'Copy all subcategories of this category and its subcategories.');
// Create the "Copy Both" button
const copyBothBtn = document.createElement('button');
copyBothBtn.textContent = 'Copy both';
copyBothBtn.style.marginRight = '10px';
copyBothBtn.style.padding = '8px 12px';
copyBothBtn.style.cursor = 'pointer';
addTooltip(copyBothBtn, 'Copy all items and subcategories from this category. Not recursive.');
// Create the "Copy Both Recursively" button
const copyBothRecursiveBtn = document.createElement('button');
copyBothRecursiveBtn.textContent = 'Copy both recursively';
copyBothRecursiveBtn.style.marginRight = '10px';
copyBothRecursiveBtn.style.padding = '8px 12px';
copyBothRecursiveBtn.style.cursor = 'pointer';
addTooltip(copyBothRecursiveBtn, 'Copy all items and subcategories from this category and all its subcategories.');
// Add checkbox for URL export
const urlCheckbox = document.createElement('input');
urlCheckbox.type = 'checkbox';
urlCheckbox.id = 'includeUrls';
urlCheckbox.style.marginLeft = '15px';
const urlLabel = document.createElement('label');
urlLabel.htmlFor = 'includeUrls';
urlLabel.textContent = 'Whole URLs';
urlLabel.style.marginLeft = '5px';
addTooltip(urlLabel, 'Include full Wikipedia URLs for each item in the export');
 
// Create status text
Line 34 ⟶ 91:
statusText.style.color = '#555';
// Add buttons to container in the requested order
container.appendChild(copyItemsBtn);
container.appendChild(copyAllItemsBtn);
container.appendChild(copyDirectSubcatsBtn);
container.appendChild(copySubcatsBtn);
container.appendChild(copyBothBtn);
container.appendChild(copyBothRecursiveBtn);
container.appendChild(urlCheckbox);
container.appendChild(urlLabel);
container.appendChild(statusText);
Line 47 ⟶ 110:
}
 
// Global visited set to prevent visiting any page more than once across all operations
// Function to get items from the current category page
const globalVisited = new Set();
function getItemsFromCurrentPage() {
 
const items = [];
// Function to format items with URLs if requested
function formatItems(items, includeUrls) {
if (!includeUrls) {
return items.join('\n');
}
// GetWhen allURLs linksare fromrequested, return ONLY the categoryURLs, pagenot thatthe aren'tarticle subcategoriesnames
return items.map(item => {
const itemLinks = Array.from(document.querySelectorAll('#mw-pages a'));
const encodedTitle = encodeURIComponent(item.replace(/ /g, '_'));
return `https://en.wikipedia.org/wiki/${encodedTitle}`;
}).join('\n');
}
 
// Function that creates a download link as an alternative to clipboard
function offerTextAsDownload(text, filename) {
// Create blob from text
const blob = new Blob([text], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
// Create download link
itemLinks.forEach(link => {
const downloadLink = document.createElement('a');
// Skip unnecessary links like "previous page" or "next page"
downloadLink.href = url;
if (link.parentElement.id === 'mw-pages-prev' ||
downloadLink.download = filename || 'wikipedia-category-items.txt';
link.parentElement.id === 'mw-pages-next' ||
linkdownloadLink.textContent.trim() === 'next`Download page'${filename || 'items'} as text file`;
linkdownloadLink.textContentstyle.trim()display === 'previous pageblock') {;
downloadLink.style.marginTop = return'10px';
}
// Skip links from the navigation sections
if (link.closest('.mw-normal-catlinks') ||
link.closest('.noprint') ||
link.closest('.catlinks')) {
return;
}
// Add the item to our list
items.push(link.textContent.trim());
});
return// items;Add to status container
statusText.appendChild(downloadLink);
return true;
}
 
// Function to copy text to clipboard (Firefoxor onoffer Popdownload OSif compatible)copying fails
function copyToClipboardcopyToClipboardOrDownload(text, categoryName) {
return new Promise((resolve, reject) => {
// CreateTry ato temporarycopy textareato elementclipboard first
tryClipboardCopy(text).then(success => {
const textarea = document.createElement('textarea');
textarea.value = text; if (success) {
resolve(true);
// Make the textarea invisible} butelse present in the DOM{
// If clipboard fails, offer download instead
textarea.style.position = 'fixed';
const filename = `${categoryName.replace(/[^a-z0-9]/gi, '_')}-items.txt`;
textarea.style.opacity = '0';
// Clear the status text completely and show only the clipboard failure message
document.body.appendChild(textarea);
statusText.innerHTML = `<p>Clipboard access failed. Click the link below to download items:</p>`;
// Select and copy offerTextAsDownload(text, filename);
textarea.select resolve(false);
}
try {});
});
// Try the modern clipboard API first
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
// Try multiple clipboard methods
document.body.removeChild(textarea);
function tryClipboardCopy(text) {
resolve(true);
return new }Promise((resolve).catch(err => {
// First try the modern Clipboard // Fall back to document.execCommandAPI
if (navigator.clipboard && navigator.clipboard.writeText) {
const success = document.execCommand('copy');
documentnavigator.bodyclipboard.removeChildwriteText(textareatext);
.then(() => if resolve(successtrue)) {
.catch(() => resolve(true);{
}// elseIf {Clipboard API fails, try execCommand
try reject(new Error('Unable to copy to clipboard'));{
const textarea = document.createElement('textarea');
textarea.value = text;
// Position off-screen but available
textarea.style.position = 'fixed';
textarea.style.left = '-999999px';
textarea.style.top = '-999999px';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
resolve(true);
} else {
resolve(false);
}
} catch (e) {
console.error("Clipboard operations failed:", e);
resolve(false);
}
});
} else {
// No clipboard API, // Usetry execCommand as fallbackdirectly
try {
const textarea = document.createElement('textarea');
textarea.value = text;
// Position off-screen but available
textarea.style.position = 'fixed';
textarea.style.left = '-999999px';
textarea.style.top = '-999999px';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
resolve(true);
} else {
rejectresolve(new Error('Unable to copy to clipboard')false);
}
} catch (e) {
console.error("Clipboard operations failed:", e);
resolve(false);
}
} catch (err) {
document.body.removeChild(textarea);
reject(err);
}
});
}
 
// Enhanced API request function with retry logic, rate limiting, and maxlag handling
// Function to get subcategories from the current page
async function getSubcategoriesmakeApiRequest(url, retryCount = 0) {
consttry subcategories = [];{
await new Promise(resolve => setTimeout(resolve, API_DELAY));
const subcategoryLinks = document.querySelectorAll('#mw-subcategories a');
const response = await fetch(url);
subcategoryLinks.forEach(link => {
// Skip "previous page" or "next page" links
// Handle rate limiting (HTTP 429) or server errors (5xx)
if (link.parentElement.id === 'mw-subcategories-prev' ||
if link(response.parentElement.idstatus === 'mw-subcategories-next'429 || response.status >= 500) {
link.textContent.trimif ()retryCount ===< 'next page'MAX_RETRIES) ||{
link.textContent const waitTime = Math.trimpow(2, retryCount) ===* 'previous1000; page')// Exponential {backoff
statusText.innerHTML += `<br>Rate limited or server error, waiting ${waitTime/1000}s before retry ${retryCount + 1}/${MAX_RETRIES}...`;
return;
await new Promise(resolve => setTimeout(resolve, waitTime));
return makeApiRequest(url, retryCount + 1);
} else {
throw new Error(`Request failed after ${MAX_RETRIES} retries: ${response.status}`);
}
}
//if Only(!response.ok) include links to category pages{
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
if (link.href && link.href.includes('/wiki/Category:')) {
subcategories.push(link.href);
}
});
const data = await response.json();
return subcategories;
// Handle maxlag errors - these don't count as retries since they're not real failures
if (data.error && data.error.code === 'maxlag') {
const lagTime = data.error.lag || 5; // Default to 5 seconds if lag not specified
const waitTime = (lagTime + 2) * 1000; // Add 2 second buffer
statusText.innerHTML += `<br>Database lagged (${lagTime}s), waiting ${waitTime/1000}s before retry...`;
await new Promise(resolve => setTimeout(resolve, waitTime));
return makeApiRequest(url, retryCount); // Don't increment retry count for maxlag
}
// Handle other API errors
if (data.error) {
throw new Error(`API Error: ${data.error.code} - ${data.error.info}`);
}
return data;
} catch (error) {
if (retryCount < MAX_RETRIES) {
statusText.innerHTML += `<br>Request failed, retrying ${retryCount + 1}/${MAX_RETRIES}...`;
await new Promise(resolve => setTimeout(resolve, 1000));
return makeApiRequest(url, retryCount + 1);
} else {
throw error;
}
}
}
 
 
// Function to fetch and process a category page
 
async function fetchCategoryPage(url) {
// Function to get all subcategories of a category
statusText.textContent = `Fetching: ${url}`;
async function getSubcategories(categoryTitle, continueToken = null) {
try {
// AddBase aAPI smallURL delayfor tosubcategories avoid(only hammeringget theitems serverwith namespace 14, which is Category)
// Add maxlag parameter to be respectful of server load
await new Promise(resolve => setTimeout(resolve, 500));
let apiUrl = `https://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:${encodeURIComponent(categoryTitle)}&cmnamespace=14&cmlimit=max&maxlag=5&format=json&origin=*`;
const// responseAdd =continue awaittoken fetch(url);if provided
constif html = await response.text(continueToken); {
apiUrl += `&cmcontinue=${continueToken}`;
}
statusText.textContent = `Fetching subcategories for: ${categoryTitle}...`;
// Create a temporary element to parse the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
//const Getdata items= fromawait the pagemakeApiRequest(apiUrl);
const items = [];
const itemLinks = tempDiv.querySelectorAll('#mw-pages a');
itemLinks.forEachif (link!data.query =>|| !data.query.categorymembers) {
//console.error("Unexpected SkipAPI unnecessaryresponse:", linksdata);
ifreturn (link.parentElement.id{ ===subcategories: 'mw-pages-prev'[], ||continueToken: null };
}
link.parentElement.id === 'mw-pages-next' ||
link.textContent.trim() === 'next page' ||
// Extract subcategories and continue token, prefix with "Category:"
link.textContent.trim() === 'previous page') {
const subcategories = data.query.categorymembers.map(member => member.title); // Keep full "Category:" prefix
return;
const nextContinueToken = data.continue ? data.continue.cmcontinue : null;
}
// Add the item to our list
items.push(link.textContent.trim());
});
return items{ subcategories, continueToken: nextContinueToken };
} catch (error) {
console.error('Error"API fetchingrequest category pageerror:'", error);
statusText.textContentinnerHTML += `<br>Error fetching ${url}subcategories: ${error.message}`;
return { subcategories: [], continueToken: null };
}
}
 
// Function to get all non-category members of a category
// Handle "Copy Items" button click
async function getNonCategoryMembers(categoryTitle, continueToken = null) {
copyItemsBtn.addEventListener('click', async () => {
try {
statusText.textContent = 'Gathering items from this category...';
// Base API URL for non-category members (exclude namespace 14, which is Category)
// Add maxlag parameter to be respectful of server load
let apiUrl = `https://en.wikipedia.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:${encodeURIComponent(categoryTitle)}&cmnamespace=0|1|2|3|4|5|6|7|8|9|10|11|12|13|15&cmlimit=max&maxlag=5&format=json&origin=*`;
// Add continue token if provided
if (continueToken) {
apiUrl += `&cmcontinue=${continueToken}`;
}
statusText.textContent = `Fetching items for: ${categoryTitle}...`;
const data = await makeApiRequest(apiUrl);
if (!data.query || !data.query.categorymembers) {
console.error("Unexpected API response:", data);
return { members: [], continueToken: null };
}
// Extract members
const members = data.query.categorymembers.map(member => member.title);
const nextContinueToken = data.continue ? data.continue.cmcontinue : null;
return { members, continueToken: nextContinueToken };
} catch (error) {
console.error("API request error:", error);
statusText.innerHTML += `<br>Error fetching items: ${error.message}`;
return { members: [], continueToken: null };
}
}
// Function to get all members of a category, handling pagination
async function getAllCategoryMembers(categoryTitle) {
let allMembers = [];
let continueToken = null;
let pagesProcessed = 0;
do {
const items = getItemsFromCurrentPage();
const { members, continueToken: nextToken } = await getNonCategoryMembers(categoryTitle, continueToken);
allMembers = allMembers.concat(members);
continueToken = nextToken;
pagesProcessed++;
statusText.innerHTML = `Retrieved ${allMembers.length} items from "${categoryTitle}" (page ${pagesProcessed})...`;
} while (continueToken);
ifreturn (items.length === 0) {allMembers;
}
statusText.textContent = 'No items found in this category.';
return;
// Function to get all subcategories of a category, handling pagination
async function getAllSubcategories(categoryTitle) {
let allSubcategories = [];
let continueToken = null;
let pagesProcessed = 0;
do {
const { subcategories, continueToken: nextToken } = await getSubcategories(categoryTitle, continueToken);
allSubcategories = allSubcategories.concat(subcategories);
continueToken = nextToken;
pagesProcessed++;
} while (continueToken);
return allSubcategories;
}
// Function to recursively get all subcategories with circular reference detection
async function getAllSubcategoriesRecursive(categoryTitle) {
const visited = new Set();
const allSubcategories = [];
const queue = [`Category:${categoryTitle}`]; // Start with prefixed category
while (queue.length > 0) {
const currentCategory = queue.shift();
// Skip if already visited (circular reference detection)
if (visited.has(currentCategory) || globalVisited.has(currentCategory)) {
continue;
}
visited.add(currentCategory);
globalVisited.add(currentCategory);
statusText.innerHTML = `Exploring subcategories (found ${allSubcategories.length} categories, queue: ${queue.length})...`;
// Get direct subcategories (remove "Category:" prefix for API call)
const categoryNameForApi = currentCategory.replace('Category:', '');
const directSubcategories = await getAllSubcategories(categoryNameForApi);
// Add new subcategories to results and queue
for (const subcategory of directSubcategories) {
if (!visited.has(subcategory) && !globalVisited.has(subcategory)) {
allSubcategories.push(subcategory);
queue.push(subcategory);
}
}
}
return allSubcategories;
}
// Function to recursively get all items from a category and all its subcategories
async function getAllItemsRecursive(categoryTitle) {
const visited = new Set();
const allItems = [];
const queue = [categoryTitle]; // Start without prefix for consistency
let totalCategories = 0;
while (queue.length > 0) {
const currentCategory = queue.shift();
const categoryKey = `Category:${currentCategory}`;
// Skip if already visited (circular reference detection)
if (visited.has(categoryKey) || globalVisited.has(categoryKey)) {
continue;
}
visited.add(categoryKey);
globalVisited.add(categoryKey);
totalCategories++;
statusText.innerHTML = `Getting items from "${currentCategory}" (processed ${totalCategories} categories, found ${allItems.length} items, queue: ${queue.length})...`;
// Get items from current category
const currentItems = await getAllCategoryMembers(currentCategory);
allItems.push(...currentItems);
// Get direct subcategories and add to queue
const directSubcategories = await getAllSubcategories(currentCategory);
for (const subcategory of directSubcategories) {
if (!visited.has(subcategory) && !globalVisited.has(subcategory)) {
// Remove "Category:" prefix for queue consistency
const subcategoryName = subcategory.replace('Category:', '');
queue.push(subcategoryName);
}
}
}
return { items: allItems, totalCategories };
}
// Function to get both items and subcategories from a category (non-recursive)
async function getBothItemsAndSubcategories(categoryTitle) {
statusText.innerHTML = 'Gathering items and subcategories from this category...';
const items = await getAllCategoryMembers(categoryTitle);
const subcategories = await getAllSubcategories(categoryTitle);
return { items, subcategories };
}
// Function to recursively get both items and subcategories from a category and all its subcategories
async function getBothItemsAndSubcategoriesRecursive(categoryTitle) {
const visited = new Set();
const allItems = [];
const allSubcategories = [];
const queue = [categoryTitle]; // Start without prefix for consistency
let totalCategories = 0;
while (queue.length > 0) {
const currentCategory = queue.shift();
const categoryKey = `Category:${currentCategory}`;
// Skip if already visited (circular reference detection)
if (visited.has(categoryKey) || globalVisited.has(categoryKey)) {
continue;
}
visited.add(categoryKey);
globalVisited.add(categoryKey);
totalCategories++;
statusText.innerHTML = `Getting items and subcategories from "${currentCategory}" (processed ${totalCategories} categories, found ${allItems.length} items, ${allSubcategories.length} subcategories, queue: ${queue.length})...`;
// Get items from current category
const currentItems = await getAllCategoryMembers(currentCategory);
allItems.push(...currentItems);
// Get direct subcategories
const directSubcategories = await getAllSubcategories(currentCategory);
// Add subcategories to results and queue
for (const subcategory of directSubcategories) {
if (!visited.has(subcategory) && !globalVisited.has(subcategory)) {
allSubcategories.push(subcategory);
// Remove "Category:" prefix for queue consistency
const subcategoryName = subcategory.replace('Category:', '');
queue.push(subcategoryName);
}
}
}
return { items: allItems, subcategories: allSubcategories, totalCategories };
}
// Handle "Copy Items" button click
copyItemsBtn.addEventListener('click', async () => {
statusText.innerHTML = 'Gathering items from this category via API...';
try {
awaitconst copyToClipboard(items.join = await getAllCategoryMembers('\n')categoryName);
statusText.textContent = `Successfully copied ${items.length} items to clipboard.`;
if (items.length === 0) {
statusText.innerHTML = 'No items found in this category.';
return;
}
const includeUrls = urlCheckbox.checked;
const formattedText = formatItems(items, includeUrls);
const copySuccess = await copyToClipboardOrDownload(formattedText, categoryName);
if (copySuccess) {
statusText.innerHTML = `Successfully copied ${items.length} items to clipboard.`;
}
} catch (error) {
statusText.textContentinnerHTML = `Error copying to clipboard: ${error.message}`;
console.error('Copy errorError:', error);
}
});
Line 210 ⟶ 543:
// Handle "Copy All Items" button click
copyAllItemsBtn.addEventListener('click', async () => {
statusText.textContentinnerHTML = 'Gathering items from this category and all subcategories recursively via API (this may take a while)...';
//try Get items from the current page{
// Clear global visited set for this operation
let allItems = getItemsFromCurrentPage();
globalVisited.clear();
// Get subcategories
// Get all items recursively
const subcategories = getSubcategories();
const { items: allItems, totalCategories } = await getAllItemsRecursive(categoryName);
const processedCategories = new Set(); // To avoid processing the same category twice
// Deduplicate items
// Process each subcategory (limited to avoid hammering the API)
const uniqueItems = [...new Set(allItems)];
const MAX_SUBCATEGORIES = 10; // Limit the number of subcategories to process
if (uniqueItems.length === 0) {
statusText.innerHTML = 'No items found in this category or its subcategories.';
return;
}
const includeUrls = urlCheckbox.checked;
const formattedText = formatItems(uniqueItems, includeUrls);
const copySuccess = await copyToClipboardOrDownload(formattedText, categoryName + '_all_recursive');
if (copySuccess) {
statusText.innerHTML = `Successfully copied ${uniqueItems.length} unique items to clipboard from ${totalCategories} categories.`;
}
} catch (error) {
statusText.innerHTML = `Error: ${error.message}`;
console.error('Error:', error);
}
});
 
// Handle "Copy Subcats from this Category" button click
copyDirectSubcatsBtn.addEventListener('click', async () => {
statusText.innerHTML = 'Gathering direct subcategories from this category via API...';
try {
for (let i = 0; i < Math.min(subcategories.length, MAX_SUBCATEGORIES); i++) {
const subcategoryUrlsubcategories = subcategories[i]await getAllSubcategories(categoryName);
if (!processedCategoriessubcategories.has(subcategoryUrl)length === 0) {
statusText.innerHTML = 'No direct subcategories found in this category.';
processedCategories.add(subcategoryUrl);
return;
const subcategoryItems = await fetchCategoryPage(subcategoryUrl);
allItems = allItems.concat(subcategoryItems);
statusText.textContent = `Processed ${i + 1}/${Math.min(subcategories.length, MAX_SUBCATEGORIES)} subcategories. Found ${allItems.length} items so far...`;
}
const includeUrls = urlCheckbox.checked;
const formattedText = formatItems(subcategories, includeUrls);
const copySuccess = await copyToClipboardOrDownload(formattedText, categoryName + '_direct_subcats');
if (copySuccess) {
statusText.innerHTML = `Successfully copied ${subcategories.length} direct subcategories to clipboard.`;
}
} catch (error) {
statusText.innerHTML = `Error: ${error.message}`;
console.error('Error:', error);
}
});
 
// Handle "Copy Subcategories" button click
copySubcatsBtn.addEventListener('click', async () => {
statusText.innerHTML = 'Gathering all subcategories recursively via API (this may take a while)...';
try {
if (subcategories.length > MAX_SUBCATEGORIES) {
// Clear global visited set for this operation
statusText.textContent += ` (Limited to ${MAX_SUBCATEGORIES} subcategories to avoid overloading. Found ${allItems.length} items.)`;
globalVisited.clear();
const allSubcategories = await getAllSubcategoriesRecursive(categoryName);
// Deduplicate subcategories
const uniqueSubcategories = [...new Set(allSubcategories)];
if (uniqueSubcategories.length === 0) {
statusText.innerHTML = 'No subcategories found.';
return;
}
const includeUrls = urlCheckbox.checked;
const formattedText = formatItems(uniqueSubcategories, includeUrls);
const copySuccess = await copyToClipboardOrDownload(formattedText, categoryName + '_subcategories');
if (copySuccess) {
statusText.innerHTML = `Successfully copied ${uniqueSubcategories.length} unique subcategories to clipboard.`;
}
} catch (error) {
statusText.innerHTML = `Error: ${error.message}`;
console.error('Error:', error);
}
});
 
// Handle "Copy Both" button click
copyBothBtn.addEventListener('click', async () => {
statusText.innerHTML = 'Gathering both items and subcategories from this category via API...';
//try Deduplicate items{
const uniqueItems{ items, subcategories } = [...newawait SetgetBothItemsAndSubcategories(allItemscategoryName)];
if (uniqueItemsitems.length === 0 && subcategories.length === 0) {
statusText.textContentinnerHTML = 'No items or subcategories found in this category or its subcategories.';
return;
}
// Combine items and subcategories
const combinedResults = [...items, ...subcategories];
const includeUrls = urlCheckbox.checked;
const formattedText = formatItems(combinedResults, includeUrls);
const copySuccess = await copyToClipboardOrDownload(formattedText, categoryName + '_both');
if (copySuccess) {
statusText.innerHTML = `Successfully copied ${items.length} items and ${subcategories.length} subcategories (${combinedResults.length} total) to clipboard.`;
}
} catch (error) {
statusText.innerHTML = `Error: ${error.message}`;
console.error('Error:', error);
}
});
 
// Handle "Copy Both Recursively" button click
copyBothRecursiveBtn.addEventListener('click', async () => {
statusText.innerHTML = 'Gathering both items and subcategories recursively via API (this may take a while)...';
try {
await// copyToClipboard(uniqueItems.join('\n'));Clear global visited set for this operation
globalVisited.clear();
statusText.textContent = `Successfully copied ${uniqueItems.length} unique items to clipboard.`;
const { items: allItems, subcategories: allSubcategories, totalCategories } = await getBothItemsAndSubcategoriesRecursive(categoryName);
// Deduplicate items and subcategories
const uniqueItems = [...new Set(allItems)];
const uniqueSubcategories = [...new Set(allSubcategories)];
if (uniqueItems.length === 0 && uniqueSubcategories.length === 0) {
statusText.innerHTML = 'No items or subcategories found in this category or its subcategories.';
return;
}
// Combine items and subcategories
const combinedResults = [...uniqueItems, ...uniqueSubcategories];
const includeUrls = urlCheckbox.checked;
const formattedText = formatItems(combinedResults, includeUrls);
const copySuccess = await copyToClipboardOrDownload(formattedText, categoryName + '_both_recursive');
if (copySuccess) {
statusText.innerHTML = `Successfully copied ${uniqueItems.length} unique items and ${uniqueSubcategories.length} unique subcategories (${combinedResults.length} total) to clipboard from ${totalCategories} categories.`;
}
} catch (error) {
statusText.textContentinnerHTML = `Error copying to clipboard: ${error.message}`;
console.error('Copy errorError:', error);
}
});
 
console.log('Wikipedia Category Copier script has been loaded successfully!');
} else {
console.log('Wikipedia Category Copier: Not a category page, script inactive.');
}