Last active
May 10, 2025 02:20
-
-
Save JamesDBartlett3/08d7a07523dbf365874b5d51b8ad519d to your computer and use it in GitHub Desktop.
Adds multi-column sorting to the Outfitting Search page on Inara.cz
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Outfitting Search Multi-Column Sort | Inara.cz Elite Dangerous | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2025-05-07 | |
| // @description Adds multi-column sorting to the Outfitting Search page on Inara.cz | |
| // @author JamesDBartlett3 | |
| // @match https://inara.cz/elite/nearest-outfitting/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=inara.cz | |
| // @grant none | |
| // ==/UserScript== | |
| document.querySelectorAll("th").forEach((th) => { | |
| // Clone the header element to remove all existing event listeners | |
| const newTh = th.cloneNode(true); | |
| th.parentNode.replaceChild(newTh, th); | |
| th = newTh; | |
| // Clear any existing pseudo-elements (before/after) that might contain chevrons | |
| th.style.setProperty("--before-content", "none"); | |
| th.style.setProperty("--after-content", "none"); | |
| // Add styles to clear ::before and ::after pseudo-elements | |
| const style = document.createElement("style"); | |
| style.textContent = ` | |
| th::before, th::after { | |
| content: none !important; | |
| display: none !important; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| // Make the header unselectable and show it's clickable | |
| th.style.userSelect = "none"; | |
| th.style.cursor = "pointer"; | |
| // Create a container for sort indicators | |
| const indicatorContainer = document.createElement("span"); | |
| indicatorContainer.className = "sort-indicator-container"; | |
| indicatorContainer.style.display = "inline-block"; | |
| indicatorContainer.style.marginLeft = "4px"; | |
| // Create chevron indicator | |
| const chevron = document.createElement("span"); | |
| chevron.textContent = " "; // Use the specified symbol | |
| chevron.style.display = "inline"; // Show by default | |
| chevron.style.fontSize = "1.1em"; | |
| chevron.style.lineHeight = "1"; | |
| chevron.style.width = "1em"; | |
| chevron.style.textAlign = "center"; | |
| chevron.style.display = "inline-block"; | |
| chevron.className = "sort-indicator"; | |
| // Create priority number indicator | |
| const priorityNumber = document.createElement("span"); | |
| priorityNumber.className = "priority-number"; | |
| priorityNumber.style.fontSize = "0.6em"; | |
| priorityNumber.style.verticalAlign = "super"; | |
| priorityNumber.style.display = "none"; | |
| priorityNumber.style.marginLeft = "2px"; | |
| // Add both indicators to the container | |
| indicatorContainer.appendChild(chevron); | |
| indicatorContainer.appendChild(priorityNumber); | |
| th.appendChild(indicatorContainer); | |
| // Initialize sort priority if not already set | |
| if (!th.dataset.sortPriority) { | |
| th.dataset.sortPriority = "0"; | |
| } | |
| th.addEventListener( | |
| "click", | |
| (event) => { | |
| // Prevent text selection and stop event propagation to other handlers | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| event.stopImmediatePropagation(); | |
| const table = th.closest("table"); | |
| const index = Array.from(th.parentNode.children).indexOf(th); | |
| const rows = Array.from( | |
| table.querySelectorAll("tbody tr, tr:nth-child(n+2)") | |
| ); | |
| // Get all header cells | |
| const allHeaders = Array.from(table.querySelectorAll("th")); | |
| // Handle sort order for this column | |
| let order = th.dataset.order || "none"; | |
| if (order === "none") { | |
| order = "asc"; | |
| } else if (order === "asc") { | |
| order = "desc"; | |
| } else { | |
| order = "none"; | |
| } | |
| th.dataset.order = order; | |
| // Update chevron appearance | |
| chevron.textContent = | |
| order === "asc" ? " " : order === "desc" ? " " : " "; | |
| chevron.style.display = "inline"; // Always show, just change the symbol | |
| // Handle sort priorities | |
| if (!event.ctrlKey) { | |
| // Reset all other headers when not using multi-column sort | |
| allHeaders.forEach((header) => { | |
| if (header !== th) { | |
| header.dataset.sortPriority = "0"; | |
| header.dataset.order = "none"; | |
| const headerChevron = header.querySelector(".sort-indicator"); | |
| const headerPriority = header.querySelector(".priority-number"); | |
| if (headerChevron) { | |
| headerChevron.textContent = " "; // Use the correct symbol for unsorted columns | |
| headerChevron.style.display = "inline"; // Keep chevron visible | |
| headerChevron.style.fontSize = "1em"; // Reset font size | |
| headerChevron.style.lineHeight = "1"; // Reset line height | |
| } | |
| if (headerPriority) headerPriority.style.display = "none"; | |
| } | |
| }); | |
| // Set this column as the primary sort | |
| if (order !== "none") { | |
| th.dataset.sortPriority = "1"; | |
| priorityNumber.textContent = "1"; | |
| priorityNumber.style.display = "inline"; | |
| } else { | |
| th.dataset.sortPriority = "0"; // Reset priority if toggled off | |
| priorityNumber.style.display = "none"; | |
| } | |
| } else { | |
| // For multi-column sort with CTRL | |
| if (order === "none") { | |
| th.dataset.sortPriority = "0"; | |
| priorityNumber.style.display = "none"; | |
| // Ensure the chevron is bidirectional when unsorted | |
| chevron.textContent = " "; | |
| } else { | |
| // Find the highest priority | |
| const maxPriority = Math.max( | |
| ...allHeaders.map((h) => parseInt(h.dataset.sortPriority || "0")) | |
| ); | |
| // Assign next priority if this is a new sort column | |
| if (th.dataset.sortPriority === "0") { | |
| th.dataset.sortPriority = (maxPriority + 1).toString(); | |
| } | |
| // Update priority number display | |
| priorityNumber.textContent = th.dataset.sortPriority; | |
| priorityNumber.style.display = "inline"; | |
| } | |
| } | |
| // Update all priority displays to ensure consistency | |
| updatePriorityDisplays(allHeaders); | |
| // Get all active sort columns with their priorities, indices and orders | |
| const sortColumns = allHeaders | |
| .map((header, idx) => ({ | |
| priority: parseInt(header.dataset.sortPriority || "0"), | |
| index: idx, | |
| order: header.dataset.order, | |
| })) | |
| .filter((col) => col.priority > 0) | |
| .sort((a, b) => a.priority - b.priority); | |
| // Sort the rows based on all active sort columns | |
| if (sortColumns.length > 0) { | |
| rows.sort((rowA, rowB) => { | |
| // Try each sort column in priority order | |
| for (const column of sortColumns) { | |
| const v1 = rowA.children[column.index].textContent | |
| .trim() | |
| .replace(/,/g, ""); | |
| const v2 = rowB.children[column.index].textContent | |
| .trim() | |
| .replace(/,/g, ""); | |
| const num1 = parseFloat(v1); | |
| const num2 = parseFloat(v2); | |
| let comparison = 0; | |
| if (!isNaN(num1) && !isNaN(num2)) { | |
| comparison = num1 - num2; | |
| } else { | |
| comparison = v1.localeCompare(v2); | |
| } | |
| // If values are not equal, return sort result | |
| if (comparison !== 0) { | |
| return column.order === "asc" ? comparison : -comparison; | |
| } | |
| // If equal, continue to next column | |
| } | |
| return 0; // All values were equal | |
| }); | |
| // Re-append rows to maintain header row | |
| rows.forEach((row) => table.appendChild(row)); | |
| } | |
| }, | |
| true | |
| ); // Use capture phase to ensure our handler runs first | |
| // Also prevent any other click handlers from running | |
| const preventOtherHandlers = (event) => { | |
| // Allow our own handler to run, but block others | |
| if (event.currentTarget === event.target) { | |
| event.stopPropagation(); | |
| } | |
| }; | |
| // Add capture phase listener to intercept events before they reach other handlers | |
| th.addEventListener("mousedown", preventOtherHandlers, true); | |
| th.addEventListener("mouseup", preventOtherHandlers, true); | |
| }); | |
| // Observer to handle dynamically added headers | |
| const observer = new MutationObserver((mutations) => { | |
| mutations.forEach((mutation) => { | |
| if (mutation.type === "childList") { | |
| const newHeaders = Array.from(mutation.addedNodes).filter( | |
| (node) => | |
| node.nodeName === "TH" || | |
| (node.nodeType === 1 && node.querySelectorAll("th").length > 0) | |
| ); | |
| if (newHeaders.length > 0) { | |
| // Run our script again for any new headers | |
| setTimeout(() => { | |
| document.querySelectorAll("th:not(.sort-enabled)").forEach((th) => { | |
| // Mark headers we've already processed | |
| th.classList.add("sort-enabled"); | |
| // Clone to remove listeners and apply our code | |
| // (Code from above for setting up headers would go here) | |
| }); | |
| }, 100); | |
| } | |
| } | |
| }); | |
| }); | |
| // Start observing the document for changes to tables | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true, | |
| }); | |
| // Helper function to update all priority displays | |
| function updatePriorityDisplays(headers) { | |
| headers.forEach((header) => { | |
| const priority = parseInt(header.dataset.sortPriority || "0"); | |
| const priorityDisplay = header.querySelector(".priority-number"); | |
| const chevronDisplay = header.querySelector(".sort-indicator"); | |
| const order = header.dataset.order || "none"; | |
| if (priorityDisplay) { | |
| if (priority > 0) { | |
| priorityDisplay.textContent = priority.toString(); | |
| priorityDisplay.style.display = "inline"; | |
| } else { | |
| priorityDisplay.style.display = "none"; | |
| } | |
| } | |
| if (chevronDisplay) { | |
| // Set appropriate chevron and make sure it's visible | |
| if (order === "asc") { | |
| chevronDisplay.textContent = " "; | |
| chevronDisplay.style.fontSize = "1.1em"; | |
| chevronDisplay.style.lineHeight = "1"; | |
| } else if (order === "desc") { | |
| chevronDisplay.textContent = " "; | |
| chevronDisplay.style.fontSize = "1.1em"; | |
| chevronDisplay.style.lineHeight = "1"; | |
| } else { | |
| chevronDisplay.textContent = " "; | |
| chevronDisplay.style.fontSize = "1em"; | |
| chevronDisplay.style.lineHeight = "1"; | |
| } | |
| chevronDisplay.style.display = "inline"; | |
| } | |
| }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment