AtCoder Language Filter

提出言語のフィルタリングと並び替え

  1. // ==UserScript==
  2. // @name AtCoder Language Filter
  3. // @namespace https://github.com/morioprog
  4. // @version 1.0.2
  5. // @description 提出言語のフィルタリングと並び替え
  6. // @author morio_prog
  7. // @match *://atcoder.jp/contests/*/tasks/*
  8. // @match *://atcoder.jp/contests/*/submit*
  9. // @match *://atcoder.jp/contests/*/custom_test*
  10. // @license CC0
  11. // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.12/js/select2.min.js
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const DEFAULT_LANGUAGE_KEY = 'defaultLang';
  19. const SAVED_LANGUAGE_KEY = 'userscript-languagefilter-savedlanguage';
  20.  
  21. function moveElementToEndOfParent($ele) {
  22. const parent = $ele.parent();
  23. $ele.detach();
  24. parent.append($ele);
  25. }
  26.  
  27. let defLang = localStorage.getItem(DEFAULT_LANGUAGE_KEY);
  28. if (defLang) defLang = defLang.replace(/[^0-9]/g, '');
  29. const $selectLanguage = $('#select-lang select');
  30.  
  31. /* Build language-map */
  32. const optMap = new Map(); // {lang: [value, data-mime]}
  33. $selectLanguage.children('option').each(function(_, e) {
  34. const $opt = $(e);
  35. optMap.set(
  36. $opt.text(),
  37. [
  38. $opt.attr('value'),
  39. $opt.attr('data-mime'),
  40. ]
  41. );
  42. $(this).remove();
  43. });
  44.  
  45. /* Add button */
  46. const buttonHtml = `<p><button type="button" class="btn btn-default btn-sm btn-auto-height" data-toggle="modal" data-target="#LangFilterModal">提出言語の選択</button></p>`;
  47. $('#main-container > div.row > div > form > div > div.editor-buttons').append(buttonHtml);
  48.  
  49. /* Add modal */
  50. const modalHtml = `
  51. <div class="modal fade" id="LangFilterModal" tabindex="-1" role="dialog" aria-labelledby="LangFilterModalLabel">
  52. <div class="modal-dialog" role="document">
  53. <div class="modal-content">
  54. <div class="modal-header">
  55. <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
  56. <h4 class="modal-title" id="LangFilterModalLabel">提出言語の選択</h4>
  57. </div>
  58. <div class="modal-body" id="langfilter-modal">
  59. <select id="langfilter-select2" multiple="multiple"></select>
  60. </div>
  61. <div class="modal-footer">
  62. <button type="button" id="langfilter-selectall" class="btn btn-info pull-left">Select All</button>
  63. <button type="button" id="langfilter-clear" class="btn btn-danger pull-left">Clear</button>
  64. <span id="langfilter-savelabel" style="margin-right:20px;"></span>
  65. <button type="button" id="langfilter-save" class="btn btn-success">Save changes</button>
  66. </div>
  67. </div>
  68. </div>
  69. </div>`;
  70. $('body').prepend(modalHtml);
  71.  
  72. const $lfsel2 = $('select#langfilter-select2');
  73. optMap.forEach(function(val, key) {
  74. $lfsel2.append(
  75. $('<option>', {
  76. value: val[0],
  77. text: key,
  78. })
  79. );
  80. });
  81.  
  82. /* Load saved languages */
  83. let savedLanguage = localStorage.getItem(SAVED_LANGUAGE_KEY);
  84. let selectedLanguage = [];
  85. if (savedLanguage) {
  86. try {
  87. savedLanguage = JSON.parse(savedLanguage);
  88. } catch (error) {
  89. savedLanguage = [];
  90. console.log(error);
  91. }
  92. $.each(savedLanguage, function(_, lang) {
  93. if (!optMap.has(lang)) return true;
  94. const val = optMap.get(lang)[0];
  95. const mim = optMap.get(lang)[1];
  96. $selectLanguage.append(
  97. $('<option>', {
  98. 'value': val,
  99. 'data-mime': mim,
  100. 'text': lang,
  101. 'selected': val == defLang,
  102. })
  103. );
  104. selectedLanguage.push(lang);
  105. const $opt = $lfsel2.find(`option[value='${val}']`);
  106. $opt.prop('selected', true);
  107. moveElementToEndOfParent($opt);
  108. });
  109. }
  110.  
  111. /* Apply Select2 */
  112. $lfsel2.select2({
  113. placeholder: 'Languages',
  114. theme: 'bootstrap',
  115. }).on('select2:select', function(evt) {
  116. const $opt = $(this).children(`option[value=${evt.params.data.id}]`);
  117. moveElementToEndOfParent($opt);
  118. $(this).trigger("change");
  119. });
  120.  
  121. const $ul = $('#langfilter-modal > span > span.selection > span > ul.select2-selection__rendered');
  122. $ul.sortable({
  123. containment: 'parent',
  124. update: function() {
  125. $(this).children("li[title]").each(function(_, li) {
  126. const $opt = $lfsel2.children('option').filter(function() {
  127. return $(this).html() == li.title;
  128. });
  129. moveElementToEndOfParent($opt);
  130. });
  131. }
  132. });
  133.  
  134. $('#langfilter-clear').on('click', function() {
  135. $lfsel2.val(null).trigger('change');
  136. });
  137.  
  138. $('#langfilter-selectall').on('click', function() {
  139. $lfsel2.children('option').prop('selected', true);
  140. $lfsel2.trigger("change");
  141. });
  142.  
  143. $('#langfilter-save').on('click', function() {
  144. selectedLanguage = [];
  145. $ul.children('li.select2-selection__choice').each(function() {
  146. selectedLanguage.push($(this).text().slice(1));
  147. });
  148. if (selectedLanguage.length === 0) {
  149. $('#langfilter-savelabel').removeClass('text-success');
  150. $('#langfilter-savelabel').addClass('text-danger');
  151. $('#langfilter-savelabel').text('Please select at least 1 language!');
  152. } else {
  153. localStorage.setItem(SAVED_LANGUAGE_KEY, JSON.stringify(selectedLanguage));
  154. $('#langfilter-savelabel').removeClass('text-danger');
  155. $('#langfilter-savelabel').addClass('text-success');
  156. $('#langfilter-savelabel').text(`Saved (${(new Date()).toLocaleString()})`);
  157. }
  158. });
  159.  
  160. })();