AtCoder Dropdown Tasks

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

Terá de instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Terá de instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão de gestão de scripts de utilizador para instalar este script.

(Já tenho um gestor de scripts de utilizador, deixe-me instalá-lo!)

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

(Já tenho um gestor de estilos de utilizador, deixe-me instalá-lo!)

// ==UserScript==
// @name         AtCoder Dropdown Tasks
// @namespace    https://atcoder.jp/
// @version      2026-07-04
// @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 table:has(time) > 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 });
    }
})();