initial commit
This commit is contained in:
540
ebay_hdd.js
Normal file
540
ebay_hdd.js
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
// Ebay Cost/TB Calculator & Sorter V2.6
|
||||||
|
// For use with Tampermonkey @require directive.
|
||||||
|
// Original author: Your Name / AI Assistant
|
||||||
|
// Description: Calculates and displays cost per TB for eBay listings, allows sorting,
|
||||||
|
// handles ambiguous titles, and includes unit tests for parsing logic.
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const DEBUG_MODE = true; // Set to true to run unit tests on load
|
||||||
|
|
||||||
|
// --- Global Variables ---
|
||||||
|
let originalOrderMap = new Map(); // Stores { itemId: originalIndex }
|
||||||
|
let isSorted = false;
|
||||||
|
let mainListParentElement = null; // Cached parent element
|
||||||
|
let observer = null; // MutationObserver instance
|
||||||
|
let observerTargetNode = null; // Node the observer is attached to
|
||||||
|
const observerConfig = { childList: true, subtree: true }; // Observer configuration
|
||||||
|
let processTimer = null; // Timer for debouncing observer callback
|
||||||
|
|
||||||
|
// --- Core Parsing Functions ---
|
||||||
|
function parseSizeAndQuantity(title) {
|
||||||
|
title = title.toUpperCase();
|
||||||
|
let totalTB = 0;
|
||||||
|
let quantity = 1; // Default to 1
|
||||||
|
let needed_description_check = false;
|
||||||
|
let individualSizeTB = 0;
|
||||||
|
|
||||||
|
// 1. Parse Quantity - More conservative approach
|
||||||
|
// Prioritize explicit "LOT OF N", "PACK OF N", "N PACK", "N LOT", "BULK N"
|
||||||
|
const explicitQtyPatterns = [
|
||||||
|
/\b(?:LOT\s+OF|LOT)\s*\(?\s*(\d+)\s*\)?/i, // e.g., "LOT OF (9)", "LOT (9)", "LOT 9"
|
||||||
|
/\b(?:LOT\s+OF|LOT)\s*\*\s*(\d+)/i, // e.g., "Lot of*10"
|
||||||
|
/\b(?:PACK\s+OF|PACK|BULK)\s*\(?\s*(\d+)\s*\)?/i, // e.g., "PACK OF 5", "5 PACK", "BULK 5"
|
||||||
|
/\b(\d+)\s*-\s*PACK\b/i, // e.g., "10-PACK"
|
||||||
|
/\b(\d+)\s*COUNT\b/i // e.g., "10 COUNT"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of explicitQtyPatterns) {
|
||||||
|
const qtyMatch = title.match(pattern);
|
||||||
|
if (qtyMatch && qtyMatch[1]) {
|
||||||
|
const parsedQty = parseInt(qtyMatch[1], 10);
|
||||||
|
// Added a sanity check for quantity (e.g., not excessively large from a model number)
|
||||||
|
if (parsedQty > 0 && parsedQty < 500) {
|
||||||
|
quantity = parsedQty;
|
||||||
|
break; // Found a valid quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parse Size
|
||||||
|
const sizeMatches = [];
|
||||||
|
// Refined regex: TB/GB must be followed by a word boundary or specific terminators (space, hyphen, comma, parens, end of string)
|
||||||
|
// This prevents matching "GB" in "6GB/S" or "6GBPS"
|
||||||
|
const sizeRegex = /(\d+(?:\.\d+)?)\s*(TB|GB)(?:\b|(?=\s|-|,|\(|\)|$))/g;
|
||||||
|
let match;
|
||||||
|
while ((match = sizeRegex.exec(title)) !== null) {
|
||||||
|
sizeMatches.push({ value: parseFloat(match[1]), unit: match[2].toUpperCase() });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizeMatches.length > 0) {
|
||||||
|
const uniqueSizesTB = [...new Set(
|
||||||
|
sizeMatches.map(sm => sm.unit === 'GB' ? sm.value / 1000 : sm.value)
|
||||||
|
)].sort((a, b) => a - b);
|
||||||
|
|
||||||
|
if (uniqueSizesTB.length > 0) {
|
||||||
|
individualSizeTB = uniqueSizesTB[0]; // Pick the smallest detected size
|
||||||
|
if (uniqueSizesTB.length > 1) {
|
||||||
|
needed_description_check = true; // Multiple different sizes mentioned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Further checks for ambiguity for needed_description_check
|
||||||
|
if (title.match(/\d+(?:\.\d+)?\s*(?:GB|TB)\s*(?:-|&|OR|TO)\s*\d+(?:\.\d+)?\s*(?:GB|TB)/i)) {
|
||||||
|
needed_description_check = true; // e.g. "120GB-250GB" or "120GB & 250GB"
|
||||||
|
}
|
||||||
|
if (quantity > 1 && title.includes("MIXED")) {
|
||||||
|
needed_description_check = true;
|
||||||
|
}
|
||||||
|
// If "CHECK DESCRIPTION" or similar is present, and there's ambiguity in quantity or size
|
||||||
|
if (title.includes("CHECK THE DESCRIPTION") || title.includes("CHECK DESCRIPTION") || title.includes("SEE DESCRIPTION")) {
|
||||||
|
if (quantity > 1 || sizeMatches.length === 0 || sizeMatches.length > 1) {
|
||||||
|
needed_description_check = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (individualSizeTB > 0) {
|
||||||
|
totalTB = individualSizeTB * quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a lot (quantity > 1) and no size could be determined (totalTB is 0), flag for check.
|
||||||
|
if (quantity > 1 && totalTB === 0) {
|
||||||
|
needed_description_check = true;
|
||||||
|
}
|
||||||
|
// If only one size was found and quantity is 1, ensure check is false unless other flags hit
|
||||||
|
if (quantity === 1 && sizeMatches.length === 1 && !needed_description_check) {
|
||||||
|
needed_description_check = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return { totalTB: parseFloat(totalTB.toFixed(4)), quantity, needed_description_check }; // Return totalTB with more precision
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePrice(priceText) {
|
||||||
|
if (priceText.toLowerCase().includes(' to ')) {
|
||||||
|
return null; // Price range, cannot calculate
|
||||||
|
}
|
||||||
|
const priceMatch = priceText.match(/\$?([\d,]+\.?\d*)/);
|
||||||
|
if (priceMatch) {
|
||||||
|
return parseFloat(priceMatch[1].replace(/,/g, ''));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Unit Testing ---
|
||||||
|
function runUnitTests() {
|
||||||
|
console.log("Ebay Cost/TB: --- Running Unit Tests for parseSizeAndQuantity ---");
|
||||||
|
const testCases = [
|
||||||
|
{ title: "LOT OF (9) MAJOR BRAND 2.5\" 7MM SSD * Kingston, Samsung, SanDisk& PNY*120-250GB", expected: { totalTB: 9 * 0.120, quantity: 9, needed_description_check: true } },
|
||||||
|
{ title: "Lot of 10 Intel 256 GB 2.5\" SATA SSD different Model check the Description", expected: { totalTB: 10 * 0.256, quantity: 10, needed_description_check: true } },
|
||||||
|
{ title: "Lot of*10 Mixed brands 240GB-256GB 2.5\" SATA SSD Drives Working & tested", expected: { totalTB: 10 * 0.240, quantity: 10, needed_description_check: true } },
|
||||||
|
{ title: "Lot of 9 SSD 120&128 GB 2.5\" SATA different brands check the description", expected: { totalTB: 9 * 0.120, quantity: 9, needed_description_check: true } },
|
||||||
|
{ title: "Bulk 5 Lot Samsung 870 EVO 500GB SSD SATA - Used - Tested Passed Smart Test", expected: { totalTB: 5 * 0.500, quantity: 5, needed_description_check: false } },
|
||||||
|
{ title: "Samsung 1.6TB NVME PCIe 3.0 x8 2.75\" SSD MZPLK1T6HCHP PM1725 Series TLC", expected: { totalTB: 1.6, quantity: 1, needed_description_check: false } },
|
||||||
|
{ title: "Brand New Crucial X6 2TB Portable External SSD (CT2000X6SSD9)", expected: { totalTB: 2.0, quantity: 1, needed_description_check: false } },
|
||||||
|
{ title: "Western Digital WD_BLACK SN850X 2TB NVMe Internal SSD", expected: { totalTB: 2.0, quantity: 1, needed_description_check: false } },
|
||||||
|
{ title: "Corsair Force Series MP600 1TB Gen4 PCIe X4 NVMe M.2 SSD Up to 4950 MB/s CSSD...", expected: { totalTB: 1.0, quantity: 1, needed_description_check: false } },
|
||||||
|
{ title: "Micron 5100 MAX 1.84TB SATA 6Gb/s 2.5\" SSD MTFDDAK1T9TCC-1AR1ZABYY", expected: { totalTB: 1.84, quantity: 1, needed_description_check: false } },
|
||||||
|
{ title: "Dell 0HGX92 1.6TB 2.5” PCIe NVMe Gen4 SSD Intel D7-P5600 SSDPF2KE016T9T HGX92 ES", expected: { totalTB: 1.6, quantity: 1, needed_description_check: false } },
|
||||||
|
{ title: "2 X 4TB SSDs Lot", expected: { totalTB: 4.0, quantity: 1, needed_description_check: false } }, // Current simplified Q parsing will result in Q=1, totalTB=4TB. To get Q=2 needs more complex logic.
|
||||||
|
{ title: "LOT OF 2X 1TB SSDs", expected: { totalTB: 2 * 1.0, quantity: 2, needed_description_check: false } }, // Should be caught by "LOT OF (N)" if "2X" is seen as "2"
|
||||||
|
{ title: "10-PACK 1TB SSD", expected: { totalTB: 10 * 1.0, quantity: 10, needed_description_check: false } },
|
||||||
|
{ title: "SSD 5 COUNT 256GB", expected: { totalTB: 5 * 0.256, quantity: 5, needed_description_check: false } },
|
||||||
|
{ title: "Single 2TB Drive", expected: { totalTB: 2.0, quantity: 1, needed_description_check: false } },
|
||||||
|
{ title: "Lot of 2 (512GB SSDs)", expected: { totalTB: 2 * 0.512, quantity: 2, needed_description_check: false } },
|
||||||
|
{ title: "Mixed Lot SSDs 120GB, 240GB, 500GB - Total 3 drives", expected: { totalTB: 0.120, quantity: 1, needed_description_check: true } } // Q parsing for "Total N drives" not implemented, smallest size taken. Q will be 1.
|
||||||
|
];
|
||||||
|
|
||||||
|
let testsPassed = 0;
|
||||||
|
let testsFailed = 0;
|
||||||
|
|
||||||
|
testCases.forEach((test, index) => {
|
||||||
|
const result = parseSizeAndQuantity(test.title);
|
||||||
|
// Using a small tolerance for floating point comparison of totalTB
|
||||||
|
const totalTBCheck = Math.abs(result.totalTB - test.expected.totalTB) < 0.0001;
|
||||||
|
const quantityCheck = result.quantity === test.expected.quantity;
|
||||||
|
const neededCheck = result.needed_description_check === test.expected.needed_description_check;
|
||||||
|
|
||||||
|
if (totalTBCheck && quantityCheck && neededCheck) {
|
||||||
|
// console.log(`Test ${index + 1}: PASSED - "${test.title}"`);
|
||||||
|
testsPassed++;
|
||||||
|
} else {
|
||||||
|
console.error(`Test ${index + 1}: FAILED - "${test.title}"`);
|
||||||
|
console.error(` Expected: totalTB=${test.expected.totalTB.toFixed(4)}, Q=${test.expected.quantity}, Check=${test.expected.needed_description_check}`);
|
||||||
|
console.error(` Actual: totalTB=${result.totalTB.toFixed(4)}, Q=${result.quantity}, Check=${result.needed_description_check}`);
|
||||||
|
testsFailed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`--- Unit Test Summary: ${testsPassed} Passed, ${testsFailed} Failed ---`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- DOM Manipulation & Display ---
|
||||||
|
function addStyles() {
|
||||||
|
const css = `
|
||||||
|
li.s-item, li.srp-results__item {
|
||||||
|
position: relative !important; overflow: visible !important; padding-bottom: 5px !important;
|
||||||
|
}
|
||||||
|
.cost-per-tb-info {
|
||||||
|
position: absolute; right: 10px; top: 40px;
|
||||||
|
background-color: rgba(0, 100, 0, 0.85); color: white;
|
||||||
|
padding: 4px 8px; border-radius: 5px; z-index: 1000;
|
||||||
|
font-size: 0.9em; font-weight: bold; text-align: center;
|
||||||
|
box-shadow: 1px 1px 3px rgba(0,0,0,0.5); transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
.cost-per-tb-info:hover { transform: scale(1.05); }
|
||||||
|
.cost-per-tb-info small.check-desc-note {
|
||||||
|
color: #FFD700; /* Gold */
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#costPerTbSortControl {
|
||||||
|
display: inline-block; margin-left: 20px; padding: 8px 12px;
|
||||||
|
border: 1px solid #ccc; border-radius: 4px; background-color: #f0f0f0;
|
||||||
|
vertical-align: middle; color: #333; cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
#costPerTbSortControl:hover { background-color: #e9e9e9; }
|
||||||
|
#costPerTbSortControl input[type="checkbox"] { margin-right: 8px; vertical-align: middle; }
|
||||||
|
#costPerTbSortControl label { cursor: pointer; vertical-align: middle; color: #333; }
|
||||||
|
`;
|
||||||
|
try {
|
||||||
|
const styleSheet = document.createElement("style");
|
||||||
|
styleSheet.type = "text/css";
|
||||||
|
styleSheet.innerText = css;
|
||||||
|
document.head.appendChild(styleSheet);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Ebay Cost/TB: Failed to add styles:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemSelector() {
|
||||||
|
if (document.querySelector('li.s-item')) return 'li.s-item';
|
||||||
|
if (document.querySelector('li.srp-results__item')) return 'li.srp-results__item';
|
||||||
|
if (document.querySelector('div.s-item')) return 'div.s-item';
|
||||||
|
return 'li[class*="s-item"], div[class*="s-item"]';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMainListParent() {
|
||||||
|
if (mainListParentElement && document.body.contains(mainListParentElement)) {
|
||||||
|
return mainListParentElement;
|
||||||
|
}
|
||||||
|
mainListParentElement = document.querySelector('ul.srp-results, div#srp-river-results ul.srp-results');
|
||||||
|
if (!mainListParentElement) {
|
||||||
|
mainListParentElement = document.querySelector('div#srp-river-results ul');
|
||||||
|
}
|
||||||
|
if (!mainListParentElement) {
|
||||||
|
const itemSelector = getItemSelector();
|
||||||
|
const containers = document.querySelectorAll('ul, div');
|
||||||
|
for (const container of containers) {
|
||||||
|
if (container.querySelector(itemSelector)) {
|
||||||
|
const directChildren = Array.from(container.children).filter(child => child.matches(itemSelector));
|
||||||
|
if (directChildren.length > 1) {
|
||||||
|
mainListParentElement = container;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mainListParentElement) {
|
||||||
|
console.error("Ebay Cost/TB: CRITICAL - Could not find main list parent element.");
|
||||||
|
}
|
||||||
|
return mainListParentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeOriginalOrder(itemsNodeList) {
|
||||||
|
if (originalOrderMap.size > 0 && itemsNodeList.length === originalOrderMap.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
originalOrderMap.clear();
|
||||||
|
itemsNodeList.forEach((itemElement, index) => {
|
||||||
|
let itemId = itemElement.id;
|
||||||
|
if (!itemId) {
|
||||||
|
const linkWithItemId = itemElement.querySelector('a[href*="/itm/"]');
|
||||||
|
if (linkWithItemId && linkWithItemId.href) {
|
||||||
|
const match = linkWithItemId.href.match(/\/itm\/(\d+)/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
itemId = "ebayitem_" + match[1];
|
||||||
|
itemElement.id = itemId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (itemId && !originalOrderMap.has(itemId)) {
|
||||||
|
originalOrderMap.set(itemId, index);
|
||||||
|
} else if (!itemId) {
|
||||||
|
// console.warn("Ebay Cost/TB: Item found without a usable ID during storeOriginalOrder.", itemElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// console.log("Ebay Cost/TB: Stored original order for", originalOrderMap.size, "items.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function processResults() {
|
||||||
|
const parent = getMainListParent();
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const itemSelector = getItemSelector();
|
||||||
|
const itemsOnPage = parent.querySelectorAll(itemSelector + ':not(.cost-per-tb-processed-flag)');
|
||||||
|
|
||||||
|
itemsOnPage.forEach(item => {
|
||||||
|
const titleElement = item.querySelector('.s-item__title, .srp-results__title');
|
||||||
|
const priceElement = item.querySelector('.s-item__price, .srp-results__price');
|
||||||
|
|
||||||
|
if (titleElement && priceElement) {
|
||||||
|
const title = titleElement.innerText;
|
||||||
|
const priceText = priceElement.innerText;
|
||||||
|
const price = parsePrice(priceText);
|
||||||
|
const parsedInfo = parseSizeAndQuantity(title); // Returns totalTB with more precision
|
||||||
|
const totalTB = parsedInfo.totalTB;
|
||||||
|
const needed_description_check = parsedInfo.needed_description_check;
|
||||||
|
|
||||||
|
item.dataset.neededDescriptionCheck = String(needed_description_check);
|
||||||
|
|
||||||
|
let displayElement = item.querySelector('.cost-per-tb-info');
|
||||||
|
if (!displayElement) {
|
||||||
|
displayElement = document.createElement('div');
|
||||||
|
displayElement.className = 'cost-per-tb-info';
|
||||||
|
const imageContainer = item.querySelector('.s-item__image-section, .s-item__image');
|
||||||
|
if (imageContainer && imageContainer.parentNode) {
|
||||||
|
imageContainer.parentNode.style.position = 'relative';
|
||||||
|
imageContainer.parentNode.appendChild(displayElement);
|
||||||
|
} else {
|
||||||
|
item.appendChild(displayElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (price !== null && totalTB > 0) {
|
||||||
|
const costPerTB = price / totalTB;
|
||||||
|
if (needed_description_check) {
|
||||||
|
item.dataset.costPerTb = '9999999'; // Sentinel for sorting
|
||||||
|
// Display totalTB.toFixed(2) for consistency in UI
|
||||||
|
displayElement.innerHTML = `$${costPerTB.toFixed(2)}*<span> / TB</span><br><small class="check-desc-note">(${totalTB.toFixed(2)} TB, Check Desc.)</small>`;
|
||||||
|
} else {
|
||||||
|
item.dataset.costPerTb = costPerTB;
|
||||||
|
displayElement.innerHTML = `$${costPerTB.toFixed(2)}<span> / TB</span><br><small>(${totalTB.toFixed(2)} TB)</small>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.dataset.costPerTb = '9999999';
|
||||||
|
let ambiguousNote = needed_description_check || (parsedInfo.quantity > 1 && totalTB === 0) ? "(Check Desc.)" : "(Details N/A)";
|
||||||
|
// Display totalTB.toFixed(2) if totalTB > 0 but price is null, else ambiguousNote
|
||||||
|
let tbDisplay = totalTB > 0 ? `(${totalTB.toFixed(2)} TB, Price N/A)` : ambiguousNote;
|
||||||
|
if (totalTB === 0 && parsedInfo.quantity > 1) tbDisplay = ambiguousNote; // Explicitly show check desc if lot with no size
|
||||||
|
else if (totalTB === 0) tbDisplay = "(Size N/A)";
|
||||||
|
|
||||||
|
|
||||||
|
displayElement.innerHTML = `<i>Details unclear</i><br><small class="check-desc-note">${tbDisplay}</small>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(displayElement.querySelector('span')) displayElement.querySelector('span').style.fontSize = '0.8em';
|
||||||
|
if(displayElement.querySelector('small')) displayElement.querySelector('small').style.fontSize = '0.7em';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
item.dataset.costPerTb = '9999999';
|
||||||
|
}
|
||||||
|
item.classList.add('cost-per-tb-processed-flag');
|
||||||
|
item.dataset.costPerTbProcessed = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
|
const allItems = parent.querySelectorAll(itemSelector);
|
||||||
|
if (originalOrderMap.size === 0 || Math.abs(originalOrderMap.size - allItems.length) > 5) {
|
||||||
|
if (allItems.length > 0) storeOriginalOrder(allItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSorted) {
|
||||||
|
sortResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortResults() {
|
||||||
|
const parent = getMainListParent();
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const itemSelector = getItemSelector();
|
||||||
|
let itemsArray = Array.from(parent.querySelectorAll(itemSelector));
|
||||||
|
|
||||||
|
if (originalOrderMap.size === 0 && itemsArray.length > 0) {
|
||||||
|
storeOriginalOrder(itemsArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsArray.sort((a, b) => {
|
||||||
|
const costA = parseFloat(a.dataset.costPerTb || '9999999');
|
||||||
|
const costB = parseFloat(b.dataset.costPerTb || '9999999');
|
||||||
|
return costA - costB;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (observer) observer.disconnect();
|
||||||
|
itemsArray.forEach(item => parent.appendChild(item));
|
||||||
|
if (observer && observerTargetNode) {
|
||||||
|
observer.observe(observerTargetNode, observerConfig);
|
||||||
|
} else if (observer && !observerTargetNode) {
|
||||||
|
setupObserver();
|
||||||
|
}
|
||||||
|
// console.log("Ebay Cost/TB: Sorted by cost per TB.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOriginalOrder() {
|
||||||
|
const parent = getMainListParent();
|
||||||
|
if (!parent) return;
|
||||||
|
if (originalOrderMap.size === 0) {
|
||||||
|
const itemSelector = getItemSelector();
|
||||||
|
const currentItems = parent.querySelectorAll(itemSelector);
|
||||||
|
if (currentItems.length > 0) storeOriginalOrder(currentItems);
|
||||||
|
if (originalOrderMap.size === 0) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemsToReorder = [];
|
||||||
|
originalOrderMap.forEach((originalIndex, itemId) => {
|
||||||
|
const itemElement = document.getElementById(itemId) || parent.querySelector(`[id="${itemId}"]`);
|
||||||
|
if (itemElement && parent.contains(itemElement)) {
|
||||||
|
itemsToReorder.push({ element: itemElement, originalIndex: originalIndex, id: itemId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemSelector = getItemSelector();
|
||||||
|
const currentItemsInDOM = parent.querySelectorAll(itemSelector);
|
||||||
|
let newItemsCount = 0;
|
||||||
|
currentItemsInDOM.forEach(domItem => {
|
||||||
|
let itemId = domItem.id;
|
||||||
|
if (!itemId) {
|
||||||
|
const linkWithItemId = domItem.querySelector('a[href*="/itm/"]');
|
||||||
|
if (linkWithItemId && linkWithItemId.href) {
|
||||||
|
const match = linkWithItemId.href.match(/\/itm\/(\d+)/);
|
||||||
|
if (match && match[1]) itemId = "ebayitem_" + match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (itemId && !originalOrderMap.has(itemId)) {
|
||||||
|
itemsToReorder.push({ element: domItem, originalIndex: 900000 + (Array.from(domItem.parentNode.children).indexOf(domItem)), id: itemId || 'new_item_' + newItemsCount++ });
|
||||||
|
} else if (!itemId && !itemsToReorder.some(entry => entry.element === domItem)) {
|
||||||
|
itemsToReorder.push({ element: domItem, originalIndex: 950000 + (Array.from(domItem.parentNode.children).indexOf(domItem)), id: 'no_id_item_' + newItemsCount++ });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itemsToReorder.sort((a, b) => a.originalIndex - b.originalIndex);
|
||||||
|
|
||||||
|
if (observer) observer.disconnect();
|
||||||
|
itemsToReorder.forEach(entry => parent.appendChild(entry.element));
|
||||||
|
if (observer && observerTargetNode) {
|
||||||
|
observer.observe(observerTargetNode, observerConfig);
|
||||||
|
} else if (observer && !observerTargetNode) {
|
||||||
|
setupObserver();
|
||||||
|
}
|
||||||
|
// console.log("Ebay Cost/TB: Restored original order.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSortToggle(event) {
|
||||||
|
const checkbox = event.currentTarget;
|
||||||
|
if (checkbox.checked) {
|
||||||
|
isSorted = true;
|
||||||
|
sortResults();
|
||||||
|
} else {
|
||||||
|
isSorted = false;
|
||||||
|
restoreOriginalOrder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSortControl() {
|
||||||
|
let controlsContainer = document.querySelector('.srp-controls__sort- σήμερα') || document.querySelector('.srp-controls__sort') || document.querySelector('div[class*="srp-controls__sort"]');
|
||||||
|
if (!controlsContainer) {
|
||||||
|
controlsContainer = document.querySelector('.srp-sort, .srp-controls');
|
||||||
|
if (controlsContainer && controlsContainer.firstChild && controlsContainer.firstChild.nodeName === "UL") {
|
||||||
|
controlsContainer = controlsContainer.firstChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!controlsContainer) {
|
||||||
|
const fallbackContainer = document.querySelector('.srp-river-main') || document.querySelector('#srp-river-results') || document.body;
|
||||||
|
if (fallbackContainer) {
|
||||||
|
controlsContainer = document.createElement('div');
|
||||||
|
controlsContainer.style.textAlign = 'center'; controlsContainer.style.margin = '10px 0';
|
||||||
|
fallbackContainer.insertBefore(controlsContainer, fallbackContainer.firstChild);
|
||||||
|
} else { return; }
|
||||||
|
}
|
||||||
|
if (document.getElementById('costPerTbSortControlWrapper')) return;
|
||||||
|
|
||||||
|
const controlWrapper = document.createElement('div');
|
||||||
|
controlWrapper.id = 'costPerTbSortControlWrapper';
|
||||||
|
controlWrapper.style.display = 'inline-block'; controlWrapper.style.marginLeft = '20px'; controlWrapper.style.verticalAlign = 'middle';
|
||||||
|
|
||||||
|
const controlDiv = document.createElement('div');
|
||||||
|
controlDiv.id = 'costPerTbSortControl';
|
||||||
|
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox'; checkbox.id = 'costPerTbSortCheckbox'; checkbox.checked = isSorted; checkbox.style.verticalAlign = 'middle';
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.htmlFor = 'costPerTbSortCheckbox'; label.innerText = 'Sort by $/TB'; label.style.verticalAlign = 'middle'; label.style.marginLeft = '5px';
|
||||||
|
|
||||||
|
controlDiv.appendChild(checkbox); controlDiv.appendChild(label);
|
||||||
|
controlWrapper.appendChild(controlDiv);
|
||||||
|
|
||||||
|
controlDiv.addEventListener('click', (e) => {
|
||||||
|
if (e.target !== checkbox) {
|
||||||
|
checkbox.checked = !checkbox.checked;
|
||||||
|
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
checkbox.addEventListener('change', handleSortToggle);
|
||||||
|
|
||||||
|
if (controlsContainer.nextSibling) {
|
||||||
|
controlsContainer.parentNode.insertBefore(controlWrapper, controlsContainer.nextSibling);
|
||||||
|
} else {
|
||||||
|
controlsContainer.parentNode.appendChild(controlWrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupObserver() {
|
||||||
|
observerTargetNode = getMainListParent();
|
||||||
|
if (!observerTargetNode) {
|
||||||
|
observerTargetNode = document.querySelector('#srp-river-results') || document.body;
|
||||||
|
}
|
||||||
|
if (!observerTargetNode) return;
|
||||||
|
|
||||||
|
const itemSelector = getItemSelector();
|
||||||
|
const callback = function(mutationsList, obs) {
|
||||||
|
let needsProcessing = false;
|
||||||
|
for(const mutation of mutationsList) {
|
||||||
|
if (mutation.type === 'childList') {
|
||||||
|
if (mutation.addedNodes.length > 0) {
|
||||||
|
mutation.addedNodes.forEach(node => {
|
||||||
|
if (node.nodeType === 1 && !node.classList.contains('cost-per-tb-info') && (node.matches && node.matches(itemSelector) || (node.querySelector && node.querySelector(itemSelector)))) {
|
||||||
|
needsProcessing = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (mutation.removedNodes.length > 0 && !isSorted) {
|
||||||
|
mutation.removedNodes.forEach(node => {
|
||||||
|
if (node.nodeType === 1 && node.matches && node.matches(itemSelector)) {
|
||||||
|
needsProcessing = true; originalOrderMap.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!document.getElementById('costPerTbSortControlWrapper')) {
|
||||||
|
const sortControlsArea = document.querySelector('.srp-controls__sort, div[class*="srp-controls__sort"], .srp-sort, .srp-controls');
|
||||||
|
if (sortControlsArea) needsProcessing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (needsProcessing) {
|
||||||
|
clearTimeout(processTimer);
|
||||||
|
processTimer = setTimeout(() => {
|
||||||
|
processResults();
|
||||||
|
addSortControl();
|
||||||
|
}, 750);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (observer) observer.disconnect();
|
||||||
|
observer = new MutationObserver(callback);
|
||||||
|
observer.observe(observerTargetNode, observerConfig);
|
||||||
|
// console.log("Ebay Cost/TB: MutationObserver set up on:", observerTargetNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
console.log("Ebay Cost/TB V2.6 (for Tampermonkey @require) starting...");
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
runUnitTests();
|
||||||
|
}
|
||||||
|
addStyles();
|
||||||
|
setTimeout(() => {
|
||||||
|
processResults();
|
||||||
|
addSortControl();
|
||||||
|
setupObserver();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||||
|
init();
|
||||||
|
} else {
|
||||||
|
window.addEventListener('load', init);
|
||||||
|
}
|
||||||
|
})();
|
Reference in New Issue
Block a user