Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save JamesDBartlett3/08d7a07523dbf365874b5d51b8ad519d to your computer and use it in GitHub Desktop.

Select an option

Save JamesDBartlett3/08d7a07523dbf365874b5d51b8ad519d to your computer and use it in GitHub Desktop.
Adds multi-column sorting to the Outfitting Search page on Inara.cz
// ==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