AtCoder ResultsPage Tweaks

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

Versione datata 30/03/2021. Vedi la nuova versione l'ultima versione.

  1. // ==UserScript==
  2. // @name AtCoder ResultsPage Tweaks
  3. // @namespace https://github.com/yukuse
  4. // @version 1.0.3
  5. // @description AtCoderの提出結果一覧画面に自動検索機能などを追加します。
  6. // @author yukuse
  7. // @include https://atcoder.jp/contests/*/submissions
  8. // @include https://atcoder.jp/contests/*/submissions?*
  9. // @grant window.jQuery
  10. // @grant window.fixTime
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. // jQueryカスタムイベントを監視・発火するためwindowのjQueryを使用
  15. jQuery(($) => {
  16. const options = {
  17. // 検索条件変更時に自動検索 on/off
  18. autoSearchOnChange: true,
  19. // 検索結果の動的読み込み on/off
  20. // NOTE: 他のユーザースクリプトとの競合注意!
  21. dynamicResult: true,
  22. // 検索条件変更時のフォーカス維持 on/off
  23. keepSelectFocus: true,
  24. // 画面表示時のフォーカス対象 '#select-task'|'#select-language'|'#select-status'|'#input-user'|null
  25. focusOnVisit: '#select-task',
  26. };
  27.  
  28. const $container = $('#main-container');
  29. const $panelSubmission = $container.find('.panel-submission');
  30.  
  31. const baseParams = {
  32. 'f.Task': '',
  33. 'f.LanguageName': '',
  34. 'f.Status': '',
  35. 'f.User': '',
  36. page: 1,
  37. orderBy: '',
  38. desc: '',
  39. };
  40.  
  41. function parseSubmissionsUrl(url) {
  42. const params = { ...baseParams };
  43. if (url) {
  44. Object.keys(params).forEach((key) => {
  45. const regexp = new RegExp(`${key}=([^&]+)`);
  46. const result = url.match(regexp);
  47. if (result) {
  48. [, params[key]] = result;
  49. }
  50. });
  51. }
  52.  
  53. return params;
  54. }
  55.  
  56. /**
  57. * 現在のURLに応じて検索結果表示を更新
  58. * TODO: ジャッジ中表示対応
  59. */
  60. function updateSearchResult() {
  61. const { href } = location;
  62. const params = parseSubmissionsUrl(href);
  63.  
  64. // 検索条件を遷移先の状態にする
  65. Object.keys(params).forEach((key) => {
  66. $panelSubmission.find(`[name="${key}"]`).val(params[key]).trigger('change');
  67. });
  68.  
  69. const $tmp = $('<div>');
  70. $tmp.load(`${href} #main-container`, '', () => {
  71. const $newTable = $tmp.find('.table-responsive, .panel-body');
  72. // テーブル置換
  73. $panelSubmission.find('.table-responsive, .panel-body').replaceWith($newTable);
  74. // ページネーション置換
  75. if ($newTable.hasClass('table-responsive')) {
  76. $container.find('.pagination').replaceWith($tmp.find('.pagination:first'));
  77. } else {
  78. $container.find('.pagination').empty();
  79. }
  80.  
  81. // 日付を表示
  82. fixTime();
  83. });
  84. }
  85.  
  86. /**
  87. * 検索条件を元にURLを更新し、結果を表示する
  88. */
  89. function showSearchResult(params) {
  90. const paramsStr = Object.keys(params).map((key) => `${key}=${params[key]}`).join('&');
  91. const url = `${location.pathname}?${paramsStr}`;
  92.  
  93. if (options.dynamicResult) {
  94. history.pushState({}, '', url);
  95.  
  96. updateSearchResult();
  97. } else {
  98. location.href = url;
  99. }
  100. }
  101.  
  102. /**
  103. * フォームに設定されたパラメータを取得
  104. */
  105. function getFormParams() {
  106. const params = { ...baseParams };
  107. Object.keys(params).forEach((key) => {
  108. params[key] = $panelSubmission.find(`[name="${key}"]`).val();
  109. // 空のキーは外す
  110. if (!params[key]) {
  111. delete params[key];
  112. }
  113. });
  114. params.page = 1;
  115.  
  116. return params;
  117. }
  118.  
  119. /**
  120. * フォームの検索条件で検索
  121. */
  122. function search() {
  123. showSearchResult(getFormParams());
  124. }
  125.  
  126. /**
  127. * 選択欄の調整
  128. * - 選択時に自動検索
  129. * - 画面表示時に選択欄自動フォーカス
  130. * - 選択時にフォーカスが飛ばないようにする
  131. */
  132. function initSelectTweaks() {
  133. // 選択欄自動フォーカス
  134. if (options.focusOnVisit) {
  135. $panelSubmission.find(options.focusOnVisit).focus();
  136. }
  137.  
  138. $panelSubmission.find('#select-task, #select-language, #select-status').on('select2:select select2:unselect', (event) => {
  139. // unselectの場合は選択状態が遅れて反映されるため、実行を遅らせる
  140. setTimeout(() => {
  141. // 選択時に自動検索
  142. if (options.autoSearchOnChange) {
  143. search();
  144. }
  145.  
  146. // 選択時にフォーカスが飛ばないようにする
  147. if (options.keepSelectFocus) {
  148. event.target.focus();
  149. }
  150. }, 0);
  151. });
  152. }
  153.  
  154. const urlRegExp = new RegExp(`${location.pathname}[^/]*$`);
  155. /**
  156. * 検索結果のリンククリック時のページ遷移をなくし、表示を動的に更新する処理に置き換え
  157. */
  158. function initLinks() {
  159. $container.on('click', '.pagination a, .panel-submission a', (event) => {
  160. const { href } = event.target;
  161. if (!urlRegExp.test(href)) {
  162. return;
  163. }
  164. // 言語リンクは除外
  165. if (/f.Language=([^&]+)/.test(href)) {
  166. return;
  167. }
  168.  
  169. event.preventDefault();
  170.  
  171. showSearchResult(parseSubmissionsUrl(href));
  172. });
  173. }
  174.  
  175. function init() {
  176. initSelectTweaks();
  177. if (options.dynamicResult) {
  178. window.addEventListener('popstate', updateSearchResult);
  179. initLinks();
  180.  
  181. // フォームの検索押下時に検索結果を動的読み込み
  182. $panelSubmission.find('form').on('submit', (event) => {
  183. event.preventDefault();
  184. search();
  185. });
  186. }
  187. }
  188.  
  189. init();
  190. });