AtCoder Submissions Sort Marker

AtCoder の提出一覧の現在のソート列に ▲ または ▼ を表示します。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         AtCoder Submissions Sort Marker
// @namespace    http://tampermonkey.net/
// @version      2026.07.07
// @description  AtCoder の提出一覧の現在のソート列に ▲ または ▼ を表示します。
// @author       Not_Leonian
// @match        https://atcoder.jp/contests/*/submissions*
// @icon         https://www.google.com/s2/favicons?domain=atcoder.jp
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(() => {
	const CONFIG = {
		defaultOrderBy: "created",
		defaultDesc: true,
		activeClassName: "ac-submissions-sort-active",
		markerClassName: "ac-submissions-sort-marker",
	};

	const PAGE_PATH_RE = /^\/contests\/[^/]+\/submissions(?:\/me)?\/?$/;

	function isSubmissionsPage() {
		return PAGE_PATH_RE.test(location.pathname);
	}

	function parseBooleanParam(value) {
		return /^(?:true|1)$/i.test(value || "");
	}

	function getCurrentSort() {
		const params = new URLSearchParams(location.search);
		const hasOrderBy = params.has("orderBy");
		const orderBy = params.get("orderBy") || CONFIG.defaultOrderBy;

		const desc = params.has("desc")
			? parseBooleanParam(params.get("desc"))
			: !hasOrderBy && orderBy === CONFIG.defaultOrderBy
				? CONFIG.defaultDesc
				: false;

		return { orderBy, desc };
	}

	function hrefOrderBy(anchor) {
		const href = anchor.getAttribute("href");
		if (!href) return null;

		try {
			return new URL(href, location.origin).searchParams.get("orderBy");
		} catch {
			return null;
		}
	}

	function clearExistingMarkers(root = document) {
		root.querySelectorAll(`.${CONFIG.markerClassName}`).forEach((node) => {
			node.remove();
		});
		root.querySelectorAll(`.${CONFIG.activeClassName}`).forEach((node) => {
			node.classList.remove(CONFIG.activeClassName);
			node.removeAttribute("aria-sort");
		});
	}

	function findSubmissionTable() {
		return document.querySelector(".panel-submission table.table");
	}

	function findHeaderCellForOrder(table, orderBy) {
		const firstHeaderRow = table.querySelector("thead tr:first-child");
		if (!firstHeaderRow) return null;

		for (const anchor of firstHeaderRow.querySelectorAll("th a[href]")) {
			if (hrefOrderBy(anchor) === orderBy) {
				return anchor.closest("th");
			}
		}

		return null;
	}

	function applySortMarker() {
		if (!isSubmissionsPage()) return;

		const table = findSubmissionTable();
		if (!table) return;

		clearExistingMarkers(table);

		const { orderBy, desc } = getCurrentSort();
		const headerCell = findHeaderCellForOrder(table, orderBy);
		if (!headerCell) return;

		headerCell.classList.add(CONFIG.activeClassName);
		headerCell.setAttribute("aria-sort", desc ? "descending" : "ascending");

		const marker = document.createElement("span");
		marker.className = CONFIG.markerClassName;
		marker.textContent = desc ? "▼" : "▲";
		marker.setAttribute("aria-hidden", "true");

		headerCell.appendChild(marker);
	}

	function injectStyle() {
		if (document.getElementById("ac-submissions-sort-marker-style")) return;

		const style = document.createElement("style");
		style.id = "ac-submissions-sort-marker-style";
		style.textContent = `
		.panel-submission table thead tr:first-child th.${CONFIG.activeClassName} {
			position: relative;
			padding-right: 1.6em !important;
		}

		.panel-submission table thead tr:first-child th .${CONFIG.markerClassName} {
			position: absolute;
			right: 0.45em;
			top: 50%;
			transform: translateY(-50%);
			font-size: 0.9em;
			line-height: 1;
			color: #333;
			pointer-events: none;
			user-select: none;
		}
	`;
		document.head.appendChild(style);
	}

	function main() {
		injectStyle();
		applySortMarker();
	}

	main();
})();