Calculator:HighLite/Nameplates
`;
// Assign elements to global variables AFTER they are injected
itemSearchInput = document.getElementById('item-search');
outputListTextarea = document.getElementById('output-list');
copyButton = document.getElementById('copy-button');
copyFeedback = document.getElementById('copy-feedback');
itemDisplayTable = document.getElementById('item-display-table'); // Get reference to the table on the main page
}
/**
* Initializes the interactive buttons and state for each item in the table.
*/
function initializeItemRows() {
const loadingIndicator = document.getElementById('loading-indicator');
const errorMessage = document.getElementById('error-message');
const calculatorControls = document.getElementById('calculator-controls');
if (!itemDisplayTable || !itemDisplayTable.tBodies[0] || itemDisplayTable.tBodies[0].rows.length === 0) {
errorMessage.textContent = 'Error: Item data table not found or empty. Please ensure your Cargo query is correct and populating the table correctly.';
errorMessage.style.display = 'block';
if (loadingIndicator) loadingIndicator.style.display = 'none';
console.error('Item display table (ID: item-display-table) not found or has no tbody/rows after waiting.');
return;
}
// Store the tbody element once it's confirmed to exist
tableTbodyElement = itemDisplayTable.tBodies[0];
const rows = Array.from(tableTbodyElement.rows); // Convert to array for easier manipulation
// Temporary map to hold consolidated items and their determined state
// Key: consolidated item name, Value: { name, state, rowElement (primary one), buttonsCell (primary one) }
const consolidatedItemsMap = new Map();
rows.forEach(row => {
const itemNameLinkCell = row.cells[1];
const itemNameLink = itemNameLinkCell ? itemNameLinkCell.querySelector('a') : null;
const originalItemName = itemNameLink ? itemNameLink.textContent.trim() : '';
if (!originalItemName) {
console.warn('Skipping row due to missing original item name:', row);
row.style.display = 'none'; // Hide rows without a name
return;
}
const consolidatedName = getConsolidatedItemName(originalItemName);
const buttonsCell = row.querySelector('.nameplate-buttons-cell');
if (!buttonsCell) {
console.warn('Skipping row due to missing buttons cell:', row);
row.style.display = 'none'; // Hide rows without a buttons cell
return;
}
// Check if we've already processed this consolidated item
if (!consolidatedItemsMap.has(consolidatedName)) {
// This is the first time we see this consolidated item name
consolidatedItemsMap.set(consolidatedName, {
name: consolidatedName,
state: -1, // Default to hide initially, will be overridden by DEFAULT_ITEM_LIST
rowElement: row, // This row will be the primary one for this consolidated item
buttonsCell: buttonsCell // This cell will host the buttons
});
// Update the displayed item name in the table
if (itemNameLink) {
itemNameLink.textContent = consolidatedName;
} else {
// Fallback if no link, though usually there should be one
itemNameLinkCell.textContent = consolidatedName;
}
// Create and append buttons to this consolidated item's primary buttonsCell
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'item-status-buttons';
buttonsContainer.dataset.itemName = consolidatedName; // For event delegation
const states = [
{ value: -1, text: 'Hide' },
{ value: 0, text: 'Default' },
{ value: 1, text: 'Priority' }
];
states.forEach(state => {
const button = document.createElement('button');
button.textContent = state.text;
button.dataset.value = state.value;
buttonsContainer.appendChild(button);
});
buttonsCell.appendChild(buttonsContainer);
} else {
// This is a duplicate of a consolidated item, so hide its row
row.style.display = 'none';
// And remove any buttons that might have been automatically rendered by Cargo for it
if (buttonsCell) {
buttonsCell.innerHTML = ''; // Clear contents of the button cell
}
}
});
// Convert the map values to an array for allItems
allItems = Array.from(consolidatedItemsMap.values());
// Apply the default list or any saved state to the consolidated items
parseAndApplyList(DEFAULT_ITEM_LIST); // This will update the 'state' in allItems
generateOutputList(); // Initial generation of the output list
if (loadingIndicator) loadingIndicator.style.display = 'none';
if (calculatorControls) calculatorControls.style.display = 'block';
// --- Attach ONE event listener to the tbody for ALL button clicks ---
tableTbodyElement.addEventListener('click', (event) => {
const clickedButton = event.target.closest('button');
const buttonsContainer = clickedButton ? clickedButton.closest('.item-status-buttons') : null;
if (clickedButton && buttonsContainer) {
const itemName = buttonsContainer.dataset.itemName;
const newState = parseInt(clickedButton.dataset.value, 10);
const itemObj = allItems.find(i => i.name === itemName);
if (itemObj) {
itemObj.state = newState;
}
// Update active classes for buttons within this specific item's container
Array.from(buttonsContainer.children).forEach(btn => {
if (parseInt(btn.dataset.value, 10) === newState) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
generateOutputList(); // Auto-update the list
}
});
}
/**
* Generates the comma-separated output list.
*/
function generateOutputList() {
const output = allItems
.map(item => `${item.name}:${item.state}`)
.join(',');
outputListTextarea.value = output;
}
/**
* Filters the item list based on search input by hiding/showing table rows.
*/
function filterItemList() {
const searchTerm = itemSearchInput.value.toLowerCase();
allItems.forEach(item => {
// We only care about the single primary row for each consolidated item for display
const row = item.rowElement;
if (row) {
if (item.name.toLowerCase().includes(searchTerm)) {
row.style.display = ''; // Show the primary row
} else {
row.style.display = 'none'; // Hide the primary row
}
}
});
}
/**
* Copies the generated list to the clipboard.
*/
function copyToClipboard() {
outputListTextarea.select();
outputListTextarea.setSelectionRange(0, 99999);
try {
document.execCommand('copy');
copyFeedback.textContent = 'Copied!';
setTimeout(() => { copyFeedback.textContent = '', 2000; });
} catch (err) {
console.error('Failed to copy text: ', err);
copyFeedback.textContent = 'Failed to copy.';
setTimeout(() => { copyFeedback.textContent = '', 2000; });
}
}
/**
* Parses a list string and applies the settings to the item buttons.
* @param {string} listString The comma-separated list of "ItemName:Value"
*/
function parseAndApplyList(listString) {
const newSettings = new Map(); // Use a Map to store parsed settings and consolidate
listString.split(',').forEach(entry => {
const parts = entry.trim().split(':');
if (parts.length === 2) {
const originalItemName = parts[0].trim();
const value = parseInt(parts[1].trim(), 10);
if (!isNaN(value) && [ -1, 0, 1 ].includes(value)) {
const consolidatedName = getConsolidatedItemName(originalItemName);
const existingValue = newSettings.has(consolidatedName) ? newSettings.get(consolidatedName) : -2; // Use -2 as a placeholder for "not set"
// Apply consolidation priority: Priority (1) > Default (0) > Hide (-1)
if (value > existingValue) { // Higher value (more priority) wins
newSettings.set(consolidatedName, value);
}
}
}
});
allItems.forEach(item => {
const currentItemName = item.name; // This is already the consolidated name
let newState = -1; // Default to Hide if not in the pasted list
if (newSettings.has(currentItemName)) {
newState = newSettings.get(currentItemName);
}
// Update the internal state
item.state = newState;
// Update the active class on the buttons for this item (only the primary one)
if (item.buttonsCell) { // Ensure buttonsCell reference exists (this will be the primary one)
const buttonsContainer = item.buttonsCell.querySelector('.item-status-buttons');
if (buttonsContainer) {
Array.from(buttonsContainer.children).forEach(btn => {
if (parseInt(btn.dataset.value, 10) === newState) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
}
}
});
// Re-generate the output list based on the new states
generateOutputList();
}
/**
* Main entry point for the widget.
*/
function initWidget() {
injectCalculatorControls(); // Inject the HTML for controls first
// Now that controls are injected, attach event listeners to them
if (itemSearchInput) {
itemSearchInput.addEventListener('input', filterItemList);
}
if (copyButton) {
copyButton.addEventListener('click', copyToClipboard);
}
// Add input listener to the output list textarea for reverse engineering
if (outputListTextarea) {
outputListTextarea.addEventListener('input', (event) => {
parseAndApplyList(event.target.value);
});
}
// Use a MutationObserver to wait for the Cargo table's tbody to be generated
const observer = new MutationObserver((mutations, obs) => {
const table = document.getElementById('item-display-table'); // This is the ID of the table on the main page
if (table && table.tBodies[0] && table.tBodies[0].rows.length > 0) {
obs.disconnect(); // Stop observing once Cargo has populated the table
initializeItemRows(); // Proceed with setting up buttons and interactivity
}
});
// Start observing the body for the main Cargo table to appear and be populated
observer.observe(document.body, { childList: true, subtree: true });
// Fallback for immediate execution if DOM is already loaded and table is present
if (document.readyState === 'complete') {
const table = document.getElementById('item-display-table');
if (table && table.tBodies[0] && table.tBodies[0].rows.length > 0) {
observer.disconnect();
initializeItemRows();
}
}
}
// Run initialization when the DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initWidget);
} else {
initWidget();
}
})();
More...