AtCoder Submissions Sort Marker

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
})();