AtCoder Dropdown Tasks

AtCoder のコンテストページにおいて、問題タブをホバーするとドロップダウンリストを表示するようにします。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         AtCoder Dropdown Tasks
// @namespace    https://atcoder.jp/
// @version      2026-04-05
// @description  AtCoder のコンテストページにおいて、問題タブをホバーするとドロップダウンリストを表示するようにします。
// @author       magurofly
// @match        https://atcoder.jp/contests/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=atcoder.jp
// @grant        unsafeWindow
// @license      CC0-1.0 Universal
// ==/UserScript==

(async function() {
    'use strict';

    if (typeof unsafeWindow.contestScreenName !== "string") return;
    const contestScreenName = unsafeWindow.contestScreenName;

    const navTabsBox = document.querySelector("#contest-nav-tabs").getBoundingClientRect()

    const styleSheet = new CSSStyleSheet();
    styleSheet.replace(`
.atcoder-dropdown-tasks:hover .dropdown-menu {
  display: block;
  overflow: auto;
  max-height: calc(100vh - ${navTabsBox.top}px - ${navTabsBox.height}px - 2em);
}

.atcoder-dropdown-task li {
  font-family: monospace;
}

.atcoder-dropdown-tasks li.current-task {
  background-color: #eeeeee;
  font-weight: bold;
}

.atcoder-dropdown-tasks li.current-task.bg-success {
  background-color: #dee7db;
  font-weight: bold;
}

.atcoder-dropdown-tasks li.current-task.bg-warning {
  background-color: #f0e5dc;
  font-weight: bold;
}

.atcoder-dropdown-tasks li.current-task a {
  font-weight: bold;
  padding-left: calc(18px - 1em);
}

.atcoder-dropdown-tasks li.current-task a::before {
  content: "\u25b6";
  padding-right: 2px;
}
    `);
    document.adoptedStyleSheets.push(styleSheet);

    const tasksDropdown = unsafeWindow.document.querySelector("#contest-nav-tabs > ul > li:nth-child(2)");
    tasksDropdown.classList.add("atcoder-dropdown-tasks");

    const tasksDropdownButton = tasksDropdown.querySelector("a");
    tasksDropdownButton.insertAdjacentHTML("beforeend", `<span class="caret"></span>`);

    const tasksDropdownContents = document.createElement("ul");
    tasksDropdownContents.className = "dropdown-menu";
    tasksDropdown.appendChild(tasksDropdownContents);

    let currentTaskURL = null;
    let matchResult;
    if (matchResult = /^\/contests\/[^\/]+\/tasks\/[^\/?]+/.exec(location.pathname)) {
        currentTaskURL = location.origin + matchResult[0];
    } else if (/^\/contests\/[^\/]+\/submissions\/\d+/.test(location.pathname)) {
        const link = document.querySelector("#main-container > div.row > div:nth-child(2) > div:nth-child(8) > table > tbody > tr:nth-child(2) > td > a");
        currentTaskURL = link.href;
    } else if (/^\/contests\/[^\/]+\/editorial\/\d+/.test(location.pathname)) {
        const link = document.querySelector("#main-container > div.row > div:nth-child(2) > h2 > a");
        currentTaskURL = link.href;
    }

    let currentLink = null;
    const tasksHTML = await (await fetch(`/contests/${contestScreenName}/tasks`, { cache: "force-cache" })).text();
    const tasksDoc = new DOMParser().parseFromString(tasksHTML, "text/html");
    for (const row of tasksDoc.querySelectorAll("#main-container > div.row > div:nth-child(2) > div.panel.panel-default.table-responsive > table > tbody > tr")) {
        const taskNum = row.cells[0].textContent;
        const taskName = row.cells[1].textContent;
        const taskURL = row.cells[0].children[0].href;
        const link = document.createElement("a");
        link.href = taskURL;
        link.textContent = `${taskNum} - ${taskName}`;
        const li = document.createElement("li");
        li.appendChild(link);
        if (taskURL == currentTaskURL) {
            li.classList.add("current-task");
            currentLink = link;
        }
        tasksDropdownContents.appendChild(li);
    }

    if (currentLink) {
        tasksDropdown.addEventListener("mouseenter", currentLink.scrollIntoView.bind(currentLink, { block: "nearest" }), { once: true });
    }
})();