GitHub Diff Files Filter

A userscript that adds filters that toggle diff & PR folders, and files by extension

2019-03-30 기준 버전입니다. 최신 버전을 확인하세요.

  1. // ==UserScript==
  2. // @name GitHub Diff Files Filter
  3. // @version 2.1.0
  4. // @description A userscript that adds filters that toggle diff & PR folders, and files by extension
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @require https://greatest.deepsurf.us/scripts/28721-mutations/code/mutations.js?version=666427
  12. // @icon https://github.githubassets.com/pinned-octocat.svg
  13. // ==/UserScript==
  14. (() => {
  15. "use strict";
  16.  
  17. // Example page: https://github.com/julmot/mark.js/pull/250/files
  18. GM_addStyle(".gdf-extension-hidden, .gdf-folder-hidden { display: none; }");
  19.  
  20. const allLabel = "\u00ABall\u00BB",
  21. rootLabel = "\u00ABroot\u00BB",
  22. noExtLabel = "\u00ABno-ext\u00BB",
  23. dotExtLabel = "\u00ABdot-files\u00BB",
  24. renameFileLabel = "\u00ABrenamed\u00BB",
  25. minFileLabel = "\u00ABmin\u00BB";
  26.  
  27. let exts = {};
  28. let folders = {};
  29.  
  30. function toggleBlocks({subgroup, type, show}) {
  31. if (type === allLabel) {
  32. // Toggle "all" blocks
  33. $$("#files div[id*='diff']").forEach(el => {
  34. el.classList.toggle(`gdf-${subgroup}-hidden`, !show);
  35. });
  36. // update filter buttons
  37. $$(`#files .gdf-${subgroup}-filter a`).forEach(el => {
  38. el.classList.toggle("selected", show);
  39. });
  40. } else if (subgroup === "folder") {
  41. Object.keys(folders).forEach(folder => {
  42. if (folders[folder].length) {
  43. show = $(`.gdf-folder-filter a[data-item=${folder}]`).classList.contains("selected");
  44. toggleGroup({group: folders[folder], subgroup, show });
  45. }
  46. });
  47. } else if (exts[type]) {
  48. toggleGroup({group: exts[type], subgroup, show});
  49. }
  50. updateAllButton(subgroup);
  51. }
  52.  
  53. function toggleGroup({group, subgroup, show}) {
  54. const files = $("#files");
  55. /* group contains an array of anchor names used to target the
  56. * hidden link added immediately above each file div container
  57. * <a name="diff-xxxxx"></a>
  58. * <div id="diff-#" class="file js-file js-details container">
  59. */
  60. group.forEach(anchor => {
  61. const file = $(`a[name="${anchor}"]`, files);
  62. if (file && file.nextElementSibling) {
  63. file.nextElementSibling.classList.toggle(`gdf-${subgroup}-hidden`, !show);
  64. }
  65. });
  66. }
  67.  
  68. function updateAllButton(subgroup) {
  69. const buttons = $(`#files .gdf-${subgroup}-filter`),
  70. filters = $$(`a:not(.gdf-${subgroup}-all)`, buttons),
  71. selected = $$(`a:not(.gdf-${subgroup}-all).selected`, buttons);
  72. // set "all" button
  73. $(`.gdf-${subgroup}-all`, buttons).classList.toggle(
  74. "selected",
  75. filters.length === selected.length
  76. );
  77. }
  78.  
  79. function getSHA(file) {
  80. return file.hash
  81. // #toc points to "a"
  82. ? file.hash.slice(1)
  83. // .pr-toolbar points to "a > div > div.filename"
  84. : file.closest("a").hash.slice(1);
  85. }
  86.  
  87. function buildList() {
  88. exts = {};
  89. folders = {};
  90. // make noExtLabel the first element in the object
  91. exts[noExtLabel] = [];
  92. exts[dotExtLabel] = [];
  93. exts[renameFileLabel] = [];
  94. exts[minFileLabel] = [];
  95. folders[rootLabel] = [];
  96. // TOC in file diffs and pr-toolbar in Pull requests
  97. $$(".file-header .file-info > a").forEach(file => {
  98. let txt = (file.title || file.textContent || "").trim(),
  99. path = txt.split("/"),
  100. filename = txt.split("/").splice(-1)[0],
  101. // test for no extension, then get extension name
  102. // regexp from https://github.com/silverwind/file-extension
  103. ext = /\./.test(filename) ? /[^./\\]*$/.exec(filename)[0] : noExtLabel,
  104. min = /\.min\./.test(filename);
  105. // Add filter for renamed files: {old path} → {new path}
  106. if (txt.indexOf(" → ") > -1) {
  107. ext = renameFileLabel;
  108. } else if (ext === filename.slice(1)) {
  109. ext = dotExtLabel;
  110. }
  111. const sha = getSHA(file);
  112. if (ext) {
  113. if (!exts[ext]) {
  114. exts[ext] = [];
  115. }
  116. exts[ext].push(sha);
  117. if (min) {
  118. exts[minFileLabel].push(sha);
  119. }
  120. }
  121. if (path.length > 1) {
  122. path.splice(-1); // remove filename
  123. path.forEach(folder => {
  124. if (!folders[folder]) {
  125. folders[folder] = [];
  126. }
  127. folders[folder].push(sha);
  128. });
  129. } else {
  130. folders[rootLabel].push(sha);
  131. }
  132. });
  133. }
  134.  
  135. function makeFilter({subgroup, label}) {
  136. const files = $("#files");
  137. let filters = 0,
  138. group = subgroup === "folder" ? folders : exts,
  139. keys = Object.keys(group),
  140. html = `${label}: <div class="BtnGroup gdf-${subgroup}-filter">`,
  141. btnClass = "btn btn-sm selected BtnGroup-item tooltipped tooltipped-n";
  142. // get length, but don't count empty arrays
  143. keys.forEach(item => {
  144. filters += group[item].length > 0 ? 1 : 0;
  145. });
  146. // Don't bother if only one extension is found
  147. if (files && filters > 1) {
  148. filters = $(`.gdf-${subgroup}-filter-wrapper`);
  149. if (!filters) {
  150. filters = document.createElement("p");
  151. filters.className = `gdf-${subgroup}-filter-wrapper`;
  152. files.insertBefore(filters, files.firstChild);
  153. filters.addEventListener("click", event => {
  154. if (event.target.nodeName === "A") {
  155. event.preventDefault();
  156. event.stopPropagation();
  157. const el = event.target;
  158. el.classList.toggle("selected");
  159. toggleBlocks({
  160. subgroup: el.dataset.subgroup,
  161. type: el.textContent.trim(),
  162. show: el.classList.contains("selected")
  163. });
  164. }
  165. });
  166. }
  167. // add a filter "all" button to the beginning
  168. html += `
  169. <a class="${btnClass} gdf-${subgroup}-all" data-subgroup="${subgroup}" data-item="${allLabel}" aria-label="Toggle all files" href="#">
  170. ${allLabel}
  171. </a>`;
  172. keys.forEach(item => {
  173. if (group[item].length) {
  174. html += `
  175. <a class="${btnClass}" aria-label="${group[item].length}" data-subgroup="${subgroup}" data-item="${item}" href="#">
  176. ${item}
  177. </a>`;
  178. }
  179. });
  180. // prepend filter buttons
  181. filters.innerHTML = html + "</div>";
  182. }
  183. }
  184.  
  185. function init() {
  186. if ($("#files.diff-view") || $(".pr-toolbar")) {
  187. buildList();
  188. makeFilter({subgroup: "folder", label: "Filter file folder"});
  189. makeFilter({subgroup: "extension", label: "Filter file extension"});
  190. }
  191. }
  192.  
  193. function $(str, el) {
  194. return (el || document).querySelector(str);
  195. }
  196.  
  197. function $$(str, el) {
  198. return [...(el || document).querySelectorAll(str)];
  199. }
  200.  
  201. document.addEventListener("ghmo:container", init);
  202. document.addEventListener("ghmo:diff", init);
  203. init();
  204.  
  205. })();