AtCoder ResultsPage Tweaks

AtCoderの提出結果一覧画面に自動検索機能などを追加します。

Per 30-03-2021. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         AtCoder ResultsPage Tweaks
// @namespace    https://github.com/yukuse
// @version      1.0.3
// @description  AtCoderの提出結果一覧画面に自動検索機能などを追加します。
// @author       yukuse
// @include      https://atcoder.jp/contests/*/submissions
// @include      https://atcoder.jp/contests/*/submissions?*
// @grant        window.jQuery
// @grant        window.fixTime
// @license      MIT
// ==/UserScript==

// jQueryカスタムイベントを監視・発火するためwindowのjQueryを使用
jQuery(($) => {
  const options = {
    // 検索条件変更時に自動検索 on/off
    autoSearchOnChange: true,
    // 検索結果の動的読み込み on/off
    // NOTE: 他のユーザースクリプトとの競合注意!
    dynamicResult: true,
    // 検索条件変更時のフォーカス維持 on/off
    keepSelectFocus: true,
    // 画面表示時のフォーカス対象 '#select-task'|'#select-language'|'#select-status'|'#input-user'|null
    focusOnVisit: '#select-task',
  };

  const $container = $('#main-container');
  const $panelSubmission = $container.find('.panel-submission');

  const baseParams = {
    'f.Task': '',
    'f.LanguageName': '',
    'f.Status': '',
    'f.User': '',
    page: 1,
    orderBy: '',
    desc: '',
  };

  function parseSubmissionsUrl(url) {
    const params = { ...baseParams };
    if (url) {
      Object.keys(params).forEach((key) => {
        const regexp = new RegExp(`${key}=([^&]+)`);
        const result = url.match(regexp);
        if (result) {
          [, params[key]] = result;
        }
      });
    }

    return params;
  }

  /**
   * 現在のURLに応じて検索結果表示を更新
   * TODO: ジャッジ中表示対応
   */
  function updateSearchResult() {
    const { href } = location;
    const params = parseSubmissionsUrl(href);

    // 検索条件を遷移先の状態にする
    Object.keys(params).forEach((key) => {
      $panelSubmission.find(`[name="${key}"]`).val(params[key]).trigger('change');
    });

    const $tmp = $('<div>');
    $tmp.load(`${href} #main-container`, '', () => {
      const $newTable = $tmp.find('.table-responsive, .panel-body');
      // テーブル置換
      $panelSubmission.find('.table-responsive, .panel-body').replaceWith($newTable);
      // ページネーション置換
      if ($newTable.hasClass('table-responsive')) {
        $container.find('.pagination').replaceWith($tmp.find('.pagination:first'));
      } else {
        $container.find('.pagination').empty();
      }

      // 日付を表示
      fixTime();
    });
  }

  /**
   * 検索条件を元にURLを更新し、結果を表示する
   */
  function showSearchResult(params) {
    const paramsStr = Object.keys(params).map((key) => `${key}=${params[key]}`).join('&');
    const url = `${location.pathname}?${paramsStr}`;

    if (options.dynamicResult) {
      history.pushState({}, '', url);

      updateSearchResult();
    } else {
      location.href = url;
    }
  }

  /**
   * フォームに設定されたパラメータを取得
   */
  function getFormParams() {
    const params = { ...baseParams };
    Object.keys(params).forEach((key) => {
      params[key] = $panelSubmission.find(`[name="${key}"]`).val();
      // 空のキーは外す
      if (!params[key]) {
        delete params[key];
      }
    });
    params.page = 1;

    return params;
  }

  /**
   * フォームの検索条件で検索
   */
  function search() {
    showSearchResult(getFormParams());
  }

  /**
   * 選択欄の調整
   * - 選択時に自動検索
   * - 画面表示時に選択欄自動フォーカス
   * - 選択時にフォーカスが飛ばないようにする
   */
  function initSelectTweaks() {
    // 選択欄自動フォーカス
    if (options.focusOnVisit) {
      $panelSubmission.find(options.focusOnVisit).focus();
    }

    $panelSubmission.find('#select-task, #select-language, #select-status').on('select2:select select2:unselect', (event) => {
      // unselectの場合は選択状態が遅れて反映されるため、実行を遅らせる
      setTimeout(() => {
        // 選択時に自動検索
        if (options.autoSearchOnChange) {
          search();
        }

        // 選択時にフォーカスが飛ばないようにする
        if (options.keepSelectFocus) {
          event.target.focus();
        }
      }, 0);
    });
  }

  const urlRegExp = new RegExp(`${location.pathname}[^/]*$`);
  /**
   * 検索結果のリンククリック時のページ遷移をなくし、表示を動的に更新する処理に置き換え
   */
  function initLinks() {
    $container.on('click', '.pagination a, .panel-submission a', (event) => {
      const { href } = event.target;
      if (!urlRegExp.test(href)) {
        return;
      }
      // 言語リンクは除外
      if (/f.Language=([^&]+)/.test(href)) {
        return;
      }

      event.preventDefault();

      showSearchResult(parseSubmissionsUrl(href));
    });
  }

  function init() {
    initSelectTweaks();
    if (options.dynamicResult) {
      window.addEventListener('popstate', updateSearchResult);
      initLinks();

      // フォームの検索押下時に検索結果を動的読み込み
      $panelSubmission.find('form').on('submit', (event) => {
        event.preventDefault();
        search();
      });
    }
  }

  init();
});